smart_brain 0.1.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 (33) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.en.md +173 -0
  4. data/README.md +173 -0
  5. data/agents/brain_assistant.rb +11 -0
  6. data/config/brain.yml +32 -0
  7. data/conversation_demo.rb +438 -0
  8. data/db/migrate/001_init.sql +98 -0
  9. data/example.rb +91 -0
  10. data/lib/smart_brain/adapters/smart_rag/direct_client.rb +47 -0
  11. data/lib/smart_brain/adapters/smart_rag/http_client.rb +61 -0
  12. data/lib/smart_brain/adapters/smart_rag/null_client.rb +22 -0
  13. data/lib/smart_brain/configuration.rb +41 -0
  14. data/lib/smart_brain/consolidator/working_summary.rb +102 -0
  15. data/lib/smart_brain/context_composer/composer.rb +75 -0
  16. data/lib/smart_brain/contracts/context_package.rb +16 -0
  17. data/lib/smart_brain/contracts/evidence_pack.rb +16 -0
  18. data/lib/smart_brain/contracts/retrieval_plan.rb +17 -0
  19. data/lib/smart_brain/event_store/in_memory.rb +103 -0
  20. data/lib/smart_brain/fusion/merger.rb +137 -0
  21. data/lib/smart_brain/memory_extractor/extractor.rb +92 -0
  22. data/lib/smart_brain/memory_store/in_memory.rb +78 -0
  23. data/lib/smart_brain/observability/tracker.rb +60 -0
  24. data/lib/smart_brain/retrieval_planner/planner.rb +122 -0
  25. data/lib/smart_brain/retrievers/exact_retriever.rb +62 -0
  26. data/lib/smart_brain/retrievers/memory_retriever.rb +30 -0
  27. data/lib/smart_brain/retrievers/relational_retriever.rb +53 -0
  28. data/lib/smart_brain/runtime.rb +195 -0
  29. data/lib/smart_brain/version.rb +5 -0
  30. data/lib/smart_brain.rb +35 -0
  31. data/templates/brain_assistant.erb +5 -0
  32. data/workers/brain_assistant.rb +9 -0
  33. metadata +283 -0
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module SmartBrain
6
+ module MemoryStore
7
+ class InMemory
8
+ OVERWRITE_TYPES = %w[preferences goals tasks].freeze
9
+
10
+ def initialize
11
+ @by_session = Hash.new { |h, k| h[k] = [] }
12
+ @entities_index = Hash.new { |h, k| h[k] = [] }
13
+ end
14
+
15
+ def upsert(extracted)
16
+ session_id = extracted.fetch(:session_id)
17
+ items = extracted.fetch(:items, [])
18
+ written = []
19
+ conflicts = []
20
+
21
+ items.each do |item|
22
+ existing = active_item(session_id: session_id, type: item[:type], key: item[:key])
23
+
24
+ if existing && item[:status] == 'retracted'
25
+ existing[:status] = 'retracted'
26
+ conflicts << { type: 'retract', key: item[:key], previous_memory_item_id: existing[:id] }
27
+ next
28
+ end
29
+
30
+ if existing && OVERWRITE_TYPES.include?(item[:type])
31
+ existing[:status] = 'superseded'
32
+ conflicts << { type: 'overwrite', key: item[:key], previous_memory_item_id: existing[:id] }
33
+ end
34
+
35
+ record = item.merge(id: SecureRandom.uuid, status: item[:status] || 'active')
36
+ by_session[session_id] << record
37
+ update_entities(session_id: session_id, record: record)
38
+ written << record.slice(:id, :type, :key, :status, :confidence)
39
+ end
40
+
41
+ { count: written.size, items: written, conflicts: conflicts }
42
+ end
43
+
44
+ def active_items(session_id:)
45
+ by_session[session_id].select { |item| item[:status] == 'active' }
46
+ end
47
+
48
+ def entities(session_id:)
49
+ entities_index[session_id]
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :by_session, :entities_index
55
+
56
+ def active_item(session_id:, type:, key:)
57
+ by_session[session_id].find { |row| row[:type] == type && row[:key] == key && row[:status] == 'active' }
58
+ end
59
+
60
+ def update_entities(session_id:, record:)
61
+ return unless record[:type] == 'entities'
62
+
63
+ value = record[:value_json]
64
+ canonical = value[:canonical] || value[:name]
65
+ existing = entities_index[session_id].find { |e| e[:canonical] == canonical && e[:kind] == value[:kind] }
66
+ return if existing
67
+
68
+ entities_index[session_id] << {
69
+ id: SecureRandom.uuid,
70
+ name: value[:name] || canonical,
71
+ kind: value[:kind] || 'other',
72
+ canonical: canonical,
73
+ memory_item_id: record[:id]
74
+ }
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartBrain
4
+ module Observability
5
+ class Tracker
6
+ def initialize
7
+ @compose_logs = []
8
+ @commit_logs = []
9
+ end
10
+
11
+ def log_compose(payload)
12
+ compose_logs << payload
13
+ end
14
+
15
+ def log_commit(payload)
16
+ commit_logs << payload
17
+ end
18
+
19
+ def snapshot
20
+ {
21
+ compose_logs: compose_logs,
22
+ commit_logs: commit_logs,
23
+ metrics: {
24
+ compose_p95_ms: p95(compose_logs.map { |l| l[:took_ms] }),
25
+ memory_resource_ratio: memory_resource_ratio,
26
+ token_over_budget_rate: token_over_budget_rate
27
+ }
28
+ }
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :compose_logs, :commit_logs
34
+
35
+ def p95(values)
36
+ values = values.compact.sort
37
+ return 0 if values.empty?
38
+
39
+ index = [(values.length * 0.95).ceil - 1, 0].max
40
+ values[index]
41
+ end
42
+
43
+ def memory_resource_ratio
44
+ selected = compose_logs.flat_map { |l| l[:selected_evidence] || [] }
45
+ return '0/0' if selected.empty?
46
+
47
+ memory = selected.count { |e| e[:source] == 'memory' }
48
+ resource = selected.count { |e| e[:source] == 'resource' }
49
+ "#{memory}/#{resource}"
50
+ end
51
+
52
+ def token_over_budget_rate
53
+ return 0.0 if compose_logs.empty?
54
+
55
+ over = compose_logs.count { |l| l[:token_used].to_i > l[:token_limit].to_i }
56
+ (over.to_f / compose_logs.length).round(3)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartBrain
4
+ module RetrievalPlanner
5
+ class Planner
6
+ RESOURCE_HINTS = ['查资料', '引用', 'reference', 'research', '标准', '论文', '文档', 'compare', '对比', '来源'].freeze
7
+
8
+ def initialize(config:)
9
+ @config = config
10
+ end
11
+
12
+ def plan(request_id:, session_id:, user_message:, agent_state:, recent_turns:, refs:)
13
+ queries = build_queries(user_message)
14
+ plan = {
15
+ version: '0.1',
16
+ request_id: request_id,
17
+ purpose: infer_purpose(user_message),
18
+ queries: queries,
19
+ global_filters: {},
20
+ budget: {
21
+ top_k: config.retrieval.fetch(:top_k, 30),
22
+ candidate_k: config.retrieval.fetch(:candidate_k, 200),
23
+ per_mode_k: {
24
+ exact: 10,
25
+ semantic: 10,
26
+ hybrid: config.retrieval.fetch(:top_k, 30),
27
+ relational: 10,
28
+ associative: 8
29
+ },
30
+ diversity: {
31
+ by_document: config.composition.dig(:diversity, :by_document) || 3,
32
+ by_source: config.composition.dig(:diversity, :by_source_uri) || 2
33
+ }
34
+ },
35
+ output: {
36
+ include_snippets: true,
37
+ max_snippet_chars: config.composition.fetch(:max_snippet_chars, 800)
38
+ },
39
+ resource_retrieval: {
40
+ enabled: resource_retrieval_enabled?(user_message: user_message, recent_turns: recent_turns, refs: refs),
41
+ reason: resource_reason(user_message: user_message, refs: refs)
42
+ },
43
+ debug: {
44
+ trace: config.observability.fetch(:trace, true),
45
+ caller: { app: 'smart_brain', session_id: session_id },
46
+ recent_turns_count: recent_turns.size,
47
+ agent_state: agent_state,
48
+ ignored_fields: []
49
+ }
50
+ }
51
+
52
+ attach_filter_hints!(plan: plan, user_message: user_message)
53
+ plan
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :config
59
+
60
+ def build_queries(user_message)
61
+ queries = [{ text: user_message, mode: 'hybrid', weight: 1.0, filters: {}, hints: {} }]
62
+ return queries unless config.retrieval.dig(:query_expansion, :enabled)
63
+
64
+ max = config.retrieval.dig(:query_expansion, :max_queries) || 8
65
+ expansions = expansion_terms(user_message).first([max - 1, 0].max)
66
+ queries.concat(expansions.map.with_index do |text, idx|
67
+ { text: text, mode: 'associative', weight: 0.8 - (idx * 0.05), filters: {}, hints: { expanded: true } }
68
+ end)
69
+ queries
70
+ end
71
+
72
+ def expansion_terms(user_message)
73
+ tokens = user_message.to_s.scan(/[[:alnum:]_\-\p{Han}]+/).uniq
74
+ return [] if tokens.size < 2
75
+
76
+ phrases = []
77
+ phrases << tokens.first(3).join(' ')
78
+ phrases << tokens.last(3).join(' ')
79
+ phrases << tokens.sort.join(' ')
80
+ phrases.uniq.reject { |p| p == user_message }
81
+ end
82
+
83
+ def infer_purpose(user_message)
84
+ lowered = user_message.downcase
85
+ return 'research' if RESOURCE_HINTS.any? { |hint| lowered.include?(hint) }
86
+
87
+ 'qa'
88
+ end
89
+
90
+ def resource_retrieval_enabled?(user_message:, recent_turns:, refs:)
91
+ mode = config.retrieval.fetch(:enable_resource_retrieval, 'auto')
92
+ return true if mode == true
93
+ return false if mode == false
94
+
95
+ lowered = user_message.downcase
96
+ return true if RESOURCE_HINTS.any? { |hint| lowered.include?(hint) }
97
+ return true if refs.any?
98
+
99
+ recent_turns.any? { |turn| turn[:content].to_s.match?(%r{https?://|\.(md|rb|txt)}) }
100
+ end
101
+
102
+ def resource_reason(user_message:, refs:)
103
+ lowered = user_message.downcase
104
+ return 'user_requested_external_evidence' if RESOURCE_HINTS.any? { |hint| lowered.include?(hint) }
105
+ return 'has_recent_refs' if refs.any?
106
+
107
+ 'auto_disabled_or_not_needed'
108
+ end
109
+
110
+ def attach_filter_hints!(plan:, user_message:)
111
+ text = user_message.downcase
112
+ if text.include?('最近') || text.include?('recent')
113
+ plan[:global_filters][:time_range] = { from: (Time.now.utc - 7 * 24 * 3600).iso8601, to: Time.now.utc.iso8601 }
114
+ end
115
+
116
+ if text.include?('smart_rag')
117
+ plan[:global_filters][:source_uri_prefix] = ['smart_rag']
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartBrain
4
+ module Retrievers
5
+ class ExactRetriever
6
+ def retrieve(query:, memory_items:, recent_turns:, limit:)
7
+ terms = tokenize(query)
8
+
9
+ memory_hits = memory_items.filter_map do |item|
10
+ haystack = "#{item[:key]} #{item[:value_json]}".downcase
11
+ score = overlap_score(terms, haystack)
12
+ next if score <= 0
13
+
14
+ {
15
+ id: item[:id],
16
+ source: 'memory',
17
+ source_uri: "smartbrain://memory/#{item[:id]}",
18
+ title: item[:key],
19
+ snippet: item[:value_json].to_s,
20
+ mode: 'exact',
21
+ score: score + (item[:confidence] || 0.5),
22
+ ref: { memory_item_id: item[:id] }
23
+ }
24
+ end
25
+
26
+ turn_hits = recent_turns.filter_map.with_index do |turn, idx|
27
+ haystack = turn[:content].to_s.downcase
28
+ score = overlap_score(terms, haystack)
29
+ next if score <= 0
30
+
31
+ {
32
+ id: "turn-#{idx}",
33
+ source: 'memory',
34
+ source_uri: 'smartbrain://recent_turn',
35
+ title: 'Recent Turn',
36
+ snippet: turn[:content].to_s,
37
+ mode: 'exact',
38
+ score: score,
39
+ ref: { turn_id: turn[:turn_id], message_id: turn[:message_id] }
40
+ }
41
+ end
42
+
43
+ (memory_hits + turn_hits).sort_by { |h| -h[:score] }.first(limit)
44
+ end
45
+
46
+ private
47
+
48
+ def tokenize(text)
49
+ text.to_s.downcase.scan(/[[:alnum:]_\-\p{Han}]+/).uniq
50
+ end
51
+
52
+ def overlap_score(terms, haystack)
53
+ return 0.0 if terms.empty?
54
+
55
+ hits = terms.count { |t| haystack.include?(t) }
56
+ return 0.0 if hits.zero?
57
+
58
+ hits.to_f / terms.length
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'exact_retriever'
4
+ require_relative 'relational_retriever'
5
+
6
+ module SmartBrain
7
+ module Retrievers
8
+ class MemoryRetriever
9
+ def initialize(config:)
10
+ @config = config
11
+ @exact = ExactRetriever.new
12
+ @relational = RelationalRetriever.new(config: config)
13
+ end
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)
18
+ relational_hits = relational.retrieve(query: query, entities: entities, refs: refs, limit: limit)
19
+
20
+ (exact_hits + relational_hits)
21
+ .sort_by { |h| -h.fetch(:score, 0.0) }
22
+ .first(limit)
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :config, :exact, :relational
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartBrain
4
+ module Retrievers
5
+ class RelationalRetriever
6
+ def initialize(config:)
7
+ @config = config
8
+ end
9
+
10
+ def retrieve(query:, entities:, refs:, limit:)
11
+ terms = query.to_s.downcase.scan(/[[:alnum:]_\-\p{Han}]+/)
12
+ entity_hits = entities.filter_map do |entity|
13
+ score = terms.count { |t| entity[:canonical].to_s.downcase.include?(t) || entity[:name].to_s.downcase.include?(t) }
14
+ next if score.zero?
15
+
16
+ {
17
+ id: "entity-#{entity[:id]}",
18
+ source: 'memory',
19
+ source_uri: "smartbrain://entity/#{entity[:id]}",
20
+ title: entity[:name],
21
+ snippet: "Entity #{entity[:kind]}: #{entity[:canonical]}",
22
+ mode: 'relational',
23
+ score: score.to_f,
24
+ ref: { memory_item_id: entity[:memory_item_id] }
25
+ }
26
+ end
27
+
28
+ ref_hits = refs.filter_map do |ref|
29
+ value = ref[:ref_uri].to_s.downcase
30
+ score = terms.count { |t| value.include?(t) }
31
+ next if score.zero?
32
+
33
+ {
34
+ id: "ref-#{ref[:id]}",
35
+ source: 'memory',
36
+ source_uri: ref[:ref_uri],
37
+ title: ref[:ref_type],
38
+ snippet: ref[:ref_meta_json].to_s,
39
+ mode: 'relational',
40
+ score: (score * 0.8).to_f,
41
+ ref: { turn_id: ref[:turn_id] }
42
+ }
43
+ end
44
+
45
+ (entity_hits + ref_hits).sort_by { |h| -h[:score] }.first(limit)
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :config
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require_relative 'contracts/retrieval_plan'
5
+ require_relative 'contracts/evidence_pack'
6
+ require_relative 'contracts/context_package'
7
+ require_relative 'observability/tracker'
8
+ require_relative 'event_store/in_memory'
9
+ require_relative 'memory_store/in_memory'
10
+ require_relative 'memory_extractor/extractor'
11
+ require_relative 'consolidator/working_summary'
12
+ require_relative 'retrieval_planner/planner'
13
+ require_relative 'retrievers/memory_retriever'
14
+ require_relative 'adapters/smart_rag/null_client'
15
+ require_relative 'fusion/merger'
16
+ require_relative 'context_composer/composer'
17
+
18
+ module SmartBrain
19
+ class Runtime
20
+ def self.build(config:, smart_rag_client: nil, clock:)
21
+ event_store = EventStore::InMemory.new
22
+ memory_store = MemoryStore::InMemory.new
23
+ new(
24
+ config: config,
25
+ clock: clock,
26
+ event_store: event_store,
27
+ memory_store: memory_store,
28
+ extractor: MemoryExtractor::Extractor.new(config: config),
29
+ consolidator: Consolidator::WorkingSummary.new(config: config, clock: clock),
30
+ planner: RetrievalPlanner::Planner.new(config: config),
31
+ memory_retriever: Retrievers::MemoryRetriever.new(config: config),
32
+ smart_rag_client: smart_rag_client || Adapters::SmartRag::NullClient.new,
33
+ merger: Fusion::Merger.new(config: config),
34
+ composer: ContextComposer::Composer.new(config: config, clock: clock),
35
+ tracker: Observability::Tracker.new
36
+ )
37
+ end
38
+
39
+ def initialize(config:, clock:, event_store:, memory_store:, extractor:, consolidator:, planner:, memory_retriever:, smart_rag_client:, merger:, composer:, tracker:)
40
+ @config = config
41
+ @clock = clock
42
+ @event_store = event_store
43
+ @memory_store = memory_store
44
+ @extractor = extractor
45
+ @consolidator = consolidator
46
+ @planner = planner
47
+ @memory_retriever = memory_retriever
48
+ @smart_rag_client = smart_rag_client
49
+ @merger = merger
50
+ @composer = composer
51
+ @tracker = tracker
52
+ end
53
+
54
+ def commit_turn(session_id:, turn_events:)
55
+ started_at = monotonic_time
56
+ now = clock.call
57
+ turn = event_store.append_turn(session_id: session_id, turn_events: turn_events, created_at: now)
58
+ entity_frequencies = event_store.entity_frequencies(
59
+ session_id: session_id,
60
+ window_turns: config.retention.dig(:entity_gate, :window_turns) || 20
61
+ )
62
+
63
+ extracted = extractor.extract(session_id: session_id, turn: turn, entity_frequencies: entity_frequencies)
64
+ write_result = memory_store.upsert(extracted)
65
+ turn_count = event_store.turns_count(session_id: session_id)
66
+ recent_turns = event_store.recent_turns(session_id: session_id, limit: config.composition.fetch(:recent_turns_max, 8))
67
+ stage_event = extracted[:items].any? { |item| item[:type] == 'decisions' || (item[:type] == 'tasks' && item.dig(:value_json, :status) == 'done') }
68
+ summary = consolidator.update(
69
+ session_id: session_id,
70
+ turn_count: turn_count,
71
+ recent_turns: recent_turns,
72
+ memory_items: memory_store.active_items(session_id: session_id),
73
+ stage_event: stage_event
74
+ )
75
+
76
+ result = {
77
+ ok: true,
78
+ commit_id: SecureRandom.uuid,
79
+ session_id: session_id,
80
+ turn_id: turn[:id],
81
+ memory_written: write_result,
82
+ summary: summary,
83
+ explain: {
84
+ retention: extracted.fetch(:explain, []),
85
+ conflicts: write_result.fetch(:conflicts, []),
86
+ summary: {
87
+ triggered: summary[:triggered],
88
+ reason: summary[:trigger_reason]
89
+ }
90
+ }
91
+ }
92
+
93
+ tracker.log_commit(
94
+ commit_id: result[:commit_id],
95
+ session_id: session_id,
96
+ turn_id: turn[:id],
97
+ memory_items: write_result[:items],
98
+ conflicts: write_result[:conflicts],
99
+ summary_triggered: summary[:triggered],
100
+ summary_reason: summary[:trigger_reason],
101
+ took_ms: elapsed_ms(started_at)
102
+ )
103
+ result
104
+ end
105
+
106
+ def compose_context(session_id:, user_message:, agent_state: {})
107
+ started_at = monotonic_time
108
+ recent_turns = event_store.recent_turns(session_id: session_id, limit: config.composition.fetch(:recent_turns_max, 8))
109
+ refs = event_store.recent_refs(session_id: session_id, limit: config.composition.fetch(:recent_turns_max, 8))
110
+ request_id = SecureRandom.uuid
111
+ plan = planner.plan(
112
+ request_id: request_id,
113
+ session_id: session_id,
114
+ user_message: user_message,
115
+ agent_state: agent_state,
116
+ recent_turns: recent_turns,
117
+ refs: refs
118
+ )
119
+ Contracts::RetrievalPlan.validate!(plan)
120
+
121
+ memory_evidence = memory_retriever.retrieve(
122
+ query: user_message,
123
+ memory_items: memory_store.active_items(session_id: session_id),
124
+ recent_turns: recent_turns,
125
+ entities: memory_store.entities(session_id: session_id),
126
+ refs: refs
127
+ )
128
+
129
+ resource_pack = if plan.dig(:resource_retrieval, :enabled)
130
+ smart_rag_client.retrieve(plan)
131
+ else
132
+ {
133
+ version: '0.1',
134
+ request_id: request_id,
135
+ plan_id: "local-#{request_id}",
136
+ generated_at: clock.call.iso8601,
137
+ evidences: [],
138
+ explain: { ignored_fields: [] },
139
+ warnings: ['resource retrieval disabled by planner']
140
+ }
141
+ end
142
+
143
+ Contracts::EvidencePack.validate!(resource_pack)
144
+ merged_bundle = merger.merge(
145
+ query: user_message,
146
+ memory_evidence: memory_evidence,
147
+ resource_evidence: resource_pack.fetch(:evidences, [])
148
+ )
149
+ merged_bundle[:ignored_fields] = Array(merged_bundle[:ignored_fields]) + Array(resource_pack.dig(:explain, :ignored_fields))
150
+
151
+ context = composer.compose(
152
+ session_id: session_id,
153
+ user_message: user_message,
154
+ plan: plan,
155
+ plan_id: resource_pack[:plan_id],
156
+ summary: consolidator.latest_summary(session_id),
157
+ recent_turns: recent_turns,
158
+ evidence_bundle: merged_bundle
159
+ )
160
+ Contracts::ContextPackage.validate!(context)
161
+
162
+ took_ms = elapsed_ms(started_at)
163
+ token_budget = context.dig(:constraints, :token_budget) || {}
164
+ tracker.log_compose(
165
+ context_id: context[:context_id],
166
+ session_id: session_id,
167
+ request_id: request_id,
168
+ plan_id: resource_pack[:plan_id],
169
+ selected_evidence: context[:evidence],
170
+ ignored_fields: context.dig(:debug, :ignored),
171
+ token_used: token_budget[:used_estimate] || 0,
172
+ token_limit: token_budget[:limit] || config.composition.fetch(:token_limit, 8192),
173
+ took_ms: took_ms
174
+ )
175
+ context
176
+ end
177
+
178
+ def diagnostics
179
+ tracker.snapshot.merge(turns: event_store.all_turns(session_id: nil))
180
+ end
181
+
182
+ private
183
+
184
+ attr_reader :config, :clock, :event_store, :memory_store, :extractor, :consolidator,
185
+ :planner, :memory_retriever, :smart_rag_client, :merger, :composer, :tracker
186
+
187
+ def monotonic_time
188
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
189
+ end
190
+
191
+ def elapsed_ms(start)
192
+ ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartBrain
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require_relative 'smart_brain/version'
5
+ require_relative 'smart_brain/configuration'
6
+ require_relative 'smart_brain/runtime'
7
+
8
+ module SmartBrain
9
+ class << self
10
+ attr_writer :runtime
11
+
12
+ def configure(config_path: nil, smart_rag_client: nil, clock: -> { Time.now.utc })
13
+ config = Configuration.load(config_path)
14
+ @runtime = Runtime.build(config: config, smart_rag_client: smart_rag_client, clock: clock)
15
+ end
16
+
17
+ def commit_turn(session_id:, turn_events:)
18
+ runtime.commit_turn(session_id: session_id, turn_events: turn_events)
19
+ end
20
+
21
+ def compose_context(session_id:, user_message:, agent_state: {})
22
+ runtime.compose_context(session_id: session_id, user_message: user_message, agent_state: agent_state)
23
+ end
24
+
25
+ def diagnostics
26
+ runtime.diagnostics
27
+ end
28
+
29
+ private
30
+
31
+ def runtime
32
+ @runtime ||= Runtime.build(config: Configuration.load, clock: -> { Time.now.utc })
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ You are given a context package produced by SmartBrain.
2
+
3
+ <%= text %>
4
+
5
+ Please answer only the latest user request.
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ SmartPrompt.define_worker :brain_assistant_worker do
4
+ use 'silicon_flow'
5
+ model 'Pro/moonshotai/Kimi-K2.5'
6
+ sys_msg 'You are a concise assistant. Use the provided context to answer the latest user message. If evidence exists, prioritize it.'
7
+ prompt :brain_assistant, { text: params[:text] }
8
+ send_msg
9
+ end