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,438 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # =============================================================================
5
+ # SmartBrain + SmartRAG + SmartAgent 真实多轮对话演示
6
+ #
7
+ # 本示例演示:
8
+ # 1. 真实调用 SmartRAG 进行文档存储和检索
9
+ # 2. 真实调用 SmartAgent 与 LLM (Ollama) 对话
10
+ # 3. SmartBrain 在对话中维持记忆和上下文
11
+ # =============================================================================
12
+
13
+ require 'logger'
14
+ require 'json'
15
+
16
+ # 环境变量配置 - 使用轨迹流动 Kimi-K2.5 模型
17
+ ENV['SMARTRAG_DB_HOST'] ||= 'localhost'
18
+ ENV['SMARTRAG_DB_PORT'] ||= '5432'
19
+ ENV['SMARTRAG_DB_NAME'] ||= 'smart_rag_development'
20
+ ENV['SMARTRAG_DB_USER'] ||= 'rag_user'
21
+ ENV['SMARTRAG_DB_PASSWORD'] ||= 'rag_pwd'
22
+ ENV['EMBEDDING_MODEL'] = 'qwen3-embedding'
23
+
24
+ # 硅基流动 API 配置
25
+ SILICON_FLOW_API_KEY = ENV['SILICON_FLOW_API_KEY']
26
+ SILICON_FLOW_ENDPOINT = ENV['SILICON_FLOW_ENDPOINT']
27
+ SILICON_FLOW_MODEL = 'Pro/moonshotai/Kimi-K2.5'
28
+
29
+ # 加载依赖
30
+ require 'smart_prompt'
31
+ require 'smart_agent'
32
+ require_relative 'lib/smart_brain'
33
+ require_relative 'lib/smart_brain/adapters/smart_rag/direct_client'
34
+
35
+ # 加载 SmartRAG
36
+ begin
37
+ require 'sequel'
38
+ Sequel.extension 'pgvector'
39
+ rescue LoadError
40
+ # pgvector extension not available
41
+ end
42
+
43
+ begin
44
+ require '/home/mlf/smart_ai/smart_rag/lib/smart_rag'
45
+ rescue LoadError => e
46
+ warn "SmartRAG load failed: #{e.message}"
47
+ exit 1
48
+ end
49
+
50
+ puts "=" * 80
51
+ puts "SmartBrain + SmartRAG + SmartAgent 真实多轮对话演示"
52
+ puts "使用模型: 硅基流动 Pro/moonshotai/Kimi-K2.5"
53
+ puts "=" * 80
54
+
55
+ # =============================================================================
56
+ # 步骤 1: 初始化 SmartRAG
57
+ # =============================================================================
58
+
59
+ puts "\n📚 初始化 SmartRAG..."
60
+
61
+ null_logger = Logger.new(File.open(File::NULL, 'w'))
62
+
63
+ rag_config = {
64
+ database: {
65
+ adapter: 'postgresql',
66
+ host: ENV['SMARTRAG_DB_HOST'] || 'localhost',
67
+ port: (ENV['SMARTRAG_DB_PORT'] || '5432').to_i,
68
+ database: ENV['SMARTRAG_DB_NAME'] || 'smart_rag_development',
69
+ user: ENV['SMARTRAG_DB_USER'] || 'rag_user',
70
+ password: ENV['SMARTRAG_DB_PASSWORD'] || 'rag_pwd'
71
+ },
72
+ llm: {
73
+ provider: 'openai',
74
+ api_key: SILICON_FLOW_API_KEY,
75
+ endpoint: SILICON_FLOW_ENDPOINT,
76
+ model: SILICON_FLOW_MODEL,
77
+ temperature: 0.3
78
+ },
79
+ # Embedding 配置 - 禁用(轨迹流动暂不支持 embedding)
80
+ embedding: {
81
+ config_path: '/home/mlf/smart_ai/smart_rag/config/llm_config.yml'
82
+ },
83
+ # 禁用所有 SmartRAG 内部日志输出
84
+ logger: null_logger
85
+ }
86
+
87
+ begin
88
+ rag = SmartRAG::SmartRAG.new(rag_config)
89
+
90
+ stats = rag.statistics
91
+ puts "✓ SmartRAG 初始化成功"
92
+ puts " - 文档数: #{stats[:document_count]}"
93
+ puts " - 段落数: #{stats[:section_count]}"
94
+ puts " - 主题数: #{stats[:topic_count]}"
95
+ rescue StandardError => e
96
+ warn "✗ SmartRAG 初始化失败: #{e.message}"
97
+ warn "请确保 PostgreSQL 正在运行且数据库已配置"
98
+ exit 1
99
+ end
100
+
101
+ # =============================================================================
102
+ # 步骤 2: 准备知识库文档(如果还没有)
103
+ # =============================================================================
104
+
105
+ puts "\n📄 检查知识库文档..."
106
+
107
+ # 定义要添加的示例文档
108
+ documents = [
109
+ {
110
+ title: "Ruby Programming Best Practices",
111
+ url: "https://example.com/ruby-best-practices",
112
+ content: <<~DOC
113
+ # Ruby Programming Best Practices
114
+
115
+ ## Naming Conventions
116
+
117
+ - Use snake_case for methods and variables: `user_name`, `total_count`
118
+ - Use CamelCase for class and module names: `UserAccount`, `OrderProcessor`
119
+ - Use SCREAMING_SNAKE_CASE for constants: `MAX_RETRIES`, `DEFAULT_TIMEOUT`
120
+
121
+ ## Code Organization
122
+
123
+ - Keep methods short and focused (under 20 lines)
124
+ - Use single responsibility principle
125
+ - Prefer composition over inheritance
126
+ - Write self-documenting code with clear method names
127
+
128
+ ## Error Handling
129
+
130
+ - Use specific exception classes
131
+ - Avoid rescuing Exception class
132
+ - Use ensure for cleanup operations
133
+ DOC
134
+ },
135
+ {
136
+ title: "PostgreSQL Performance Optimization",
137
+ url: "https://example.com/postgresql-performance",
138
+ content: <<~DOC
139
+ # PostgreSQL Performance Optimization
140
+
141
+ ## Connection Pooling
142
+
143
+ - Use connection poolers like PgBouncer for high-concurrency applications
144
+ - Recommended pool size: (core_count * 2) + effective_spindle_count
145
+ - Monitor connection usage with pg_stat_activity
146
+
147
+ ## Indexing Strategies
148
+
149
+ - Create indexes on frequently queried columns
150
+ - Use partial indexes for filtered queries
151
+ - Consider covering indexes for index-only scans
152
+ - Regularly analyze tables for query planner
153
+
154
+ ## Query Optimization
155
+
156
+ - Use EXPLAIN ANALYZE to understand query plans
157
+ - Avoid SELECT * in production queries
158
+ - Use appropriate data types for columns
159
+ DOC
160
+ },
161
+ {
162
+ title: "Introduction to pgvector",
163
+ url: "https://example.com/pgvector-intro",
164
+ content: <<~DOC
165
+ # Introduction to pgvector
166
+
167
+ ## Overview
168
+
169
+ pgvector is a PostgreSQL extension for vector similarity search.
170
+ It allows you to store and query high-dimensional vectors directly in PostgreSQL.
171
+
172
+ ## Key Features
173
+
174
+ - Vector data type with up to 16,000 dimensions
175
+ - ivfflat and hnsw indexes for fast approximate nearest neighbor search
176
+ - L2, inner product, and cosine distance metrics
177
+ - ACID compliance through PostgreSQL
178
+
179
+ ## Installation
180
+
181
+ CREATE EXTENSION pgvector;
182
+
183
+ CREATE TABLE items (id bigserial PRIMARY KEY, embedding vector(3));
184
+
185
+ ## Use Cases
186
+
187
+ - Semantic search for AI applications
188
+ - Recommendation systems
189
+ - Image similarity search
190
+ - Document clustering
191
+ DOC
192
+ }
193
+ ]
194
+
195
+ # 检查并添加文档
196
+ documents.each do |doc|
197
+ # 检查是否已存在
198
+ existing = rag.list_documents(search: doc[:title])
199
+ if existing[:documents].any? { |d| d[:title] == doc[:title] }
200
+ puts " ✓ 文档已存在: #{doc[:title]}"
201
+ else
202
+ begin
203
+ # 创建临时文件
204
+ require 'tempfile'
205
+ Tempfile.create(['doc', '.md']) do |file|
206
+ file.write(doc[:content])
207
+ file.flush
208
+
209
+ result = rag.add_document(
210
+ file.path,
211
+ title: doc[:title],
212
+ url: doc[:url],
213
+ generate_embeddings: false,
214
+ generate_tags: false
215
+ )
216
+ puts " ✓ 添加文档: #{doc[:title]} (ID: #{result[:document_id]})"
217
+ end
218
+ rescue StandardError => e
219
+ puts " ✗ 添加失败: #{doc[:title]} - #{e.message}"
220
+ end
221
+ end
222
+ end
223
+
224
+ # =============================================================================
225
+ # 步骤 3: 初始化 SmartBrain 和 SmartAgent
226
+ # =============================================================================
227
+
228
+ puts "\n🧠 初始化 SmartBrain..."
229
+
230
+ rag_client = SmartBrain::Adapters::SmartRag::DirectClient.new(rag: rag)
231
+ SmartBrain.configure(smart_rag_client: rag_client)
232
+ puts "✓ SmartBrain 初始化成功"
233
+
234
+ puts "\n🤖 初始化 SmartAgent..."
235
+ engine = SmartAgent::Engine.new('./config/example_agent.yml')
236
+ agent = engine.build_agent(:brain_assistant)
237
+ puts "✓ SmartAgent 初始化成功"
238
+ puts " - 使用模型: #{SILICON_FLOW_MODEL}"
239
+
240
+ # =============================================================================
241
+ # 步骤 4: 定义多轮对话
242
+ # =============================================================================
243
+
244
+ session_id = "real-demo-#{Time.now.to_i}"
245
+
246
+ puts "\n" + "=" * 80
247
+ puts "开始多轮对话 (Session: #{session_id})"
248
+ puts "=" * 80
249
+
250
+ # 定义对话流程
251
+ conversations = [
252
+ {
253
+ turn: 1,
254
+ user_message: "你好,我正在学习 Ruby,想了解一下命名规范。",
255
+ extract_events: {
256
+ goals: [
257
+ { key: 'goal:learn:ruby', goal: '学习 Ruby 编程规范' }
258
+ ],
259
+ entities: [
260
+ { key: 'entity:lang:ruby', name: 'Ruby', canonical: 'ruby', kind: 'language', remember: true }
261
+ ]
262
+ }
263
+ },
264
+ {
265
+ turn: 2,
266
+ user_message: "类名应该用什么风格?",
267
+ extract_events: {
268
+ decisions: [
269
+ { key: 'decision:ruby:class_naming', decision: 'Ruby 类名使用 CamelCase' }
270
+ ]
271
+ }
272
+ },
273
+ {
274
+ turn: 3,
275
+ user_message: "明白了。现在我打算用 PostgreSQL 作为数据库,有什么性能建议吗?",
276
+ extract_events: {
277
+ goals: [
278
+ { key: 'goal:learn:postgresql', goal: '学习 PostgreSQL 性能优化' }
279
+ ],
280
+ entities: [
281
+ { key: 'entity:db:postgresql', name: 'PostgreSQL', canonical: 'postgresql', kind: 'database', remember: true }
282
+ ]
283
+ }
284
+ },
285
+ {
286
+ turn: 4,
287
+ user_message: "连接池大小一般怎么设置?",
288
+ extract_events: {
289
+ decisions: [
290
+ { key: 'decision:pg:pool_size', decision: '连接池大小公式: (core_count * 2) + effective_spindle_count' }
291
+ ]
292
+ }
293
+ },
294
+ {
295
+ turn: 5,
296
+ user_message: "我听说有个叫 pgvector 的扩展,它适合什么场景?",
297
+ extract_events: {
298
+ entities: [
299
+ { key: 'entity:ext:pgvector', name: 'pgvector', canonical: 'pgvector', kind: 'extension', remember: true }
300
+ ]
301
+ }
302
+ }
303
+ ]
304
+
305
+ # =============================================================================
306
+ # 步骤 5: 执行多轮对话
307
+ # =============================================================================
308
+
309
+ def build_worker_input(context, user_message)
310
+ evidence_text = ""
311
+ if context[:evidence] && !context[:evidence].empty?
312
+ evidence_text = "\n\n## Evidence from Knowledge Base\n\n"
313
+ context[:evidence].first(3).each_with_index do |ev, idx|
314
+ evidence_text += "#{idx + 1}. #{ev[:title]}\n"
315
+ evidence_text += " #{ev[:snippet]}\n\n"
316
+ end
317
+ end
318
+
319
+ # working_summary is a String, not a Hash
320
+ summary_text = ""
321
+ if context[:working_summary] && context[:working_summary].is_a?(String)
322
+ summary_text = "\n\n## Conversation Summary\n\n#{context[:working_summary]}"
323
+ end
324
+
325
+ # Build the text parameter for the template
326
+ context_text = "## User Message\n#{user_message}\n"
327
+ context_text += summary_text if summary_text && !summary_text.empty?
328
+ context_text += evidence_text if evidence_text && !evidence_text.empty?
329
+
330
+ # Return string (agent.please expects a string)
331
+ context_text
332
+ end
333
+
334
+ conversations.each do |conv|
335
+ puts "\n" + "-" * 80
336
+ puts "【第 #{conv[:turn]} 轮】"
337
+ puts "-" * 80
338
+
339
+ user_message = conv[:user_message]
340
+ puts "\n👤 用户: #{user_message}"
341
+
342
+ # 1. SmartBrain 组合上下文
343
+ context = SmartBrain.compose_context(
344
+ session_id: session_id,
345
+ user_message: user_message,
346
+ agent_state: { turn: conv[:turn] }
347
+ )
348
+
349
+ # 显示检索结果
350
+ if context[:evidence] && !context[:evidence].empty?
351
+ puts "\n🔍 SmartBrain 检索结果:"
352
+ memory_count = context[:evidence].count { |e| e[:source] == 'memory' }
353
+ resource_count = context[:evidence].count { |e| e[:source] == 'resource' }
354
+ puts " 记忆: #{memory_count} | 资源: #{resource_count}"
355
+
356
+ context[:evidence].first(3).each do |ev|
357
+ icon = ev[:source] == 'memory' ? '💭' : '📄'
358
+ puts " #{icon} #{ev[:title]} (score: #{(ev[:score] || 0).round(2)})"
359
+ end
360
+ end
361
+
362
+ # 2. SmartAgent 调用 LLM
363
+ puts "\n🤖 SmartAgent 调用 LLM..."
364
+ begin
365
+ assistant_response = agent.please(build_worker_input(context, user_message))
366
+
367
+ # 只显示响应的前一部分,避免输出过长
368
+ display_text = assistant_response.to_s.strip
369
+ if display_text.length > 300
370
+ display_text = display_text[0..300] + "..."
371
+ end
372
+ puts "\n📝 助手回复:"
373
+ puts " #{display_text.gsub("\n", "\n ")}"
374
+ rescue StandardError => e
375
+ puts " ✗ LLM 调用失败: #{e.message}"
376
+ puts " 错误类型: #{e.class}"
377
+ puts " 堆栈: #{e.backtrace.first(5).join("\n ")}"
378
+ assistant_response = "抱歉,我暂时无法回答这个问题。"
379
+ end
380
+
381
+ # 3. SmartBrain 提交本轮
382
+ turn_events = {
383
+ messages: [
384
+ { role: 'user', content: user_message },
385
+ { role: 'assistant', content: assistant_response.to_s }
386
+ ]
387
+ }
388
+
389
+ # 添加提取的事件
390
+ if conv[:extract_events]
391
+ turn_events.merge!(conv[:extract_events])
392
+ end
393
+
394
+ commit = SmartBrain.commit_turn(
395
+ session_id: session_id,
396
+ turn_events: turn_events
397
+ )
398
+
399
+ puts "\n💾 SmartBrain 提交:"
400
+ puts " - commit_id: #{commit[:commit_id][0..7]}..."
401
+ puts " - 记忆项: #{commit[:memory_written] ? commit[:memory_written][:count] : 0} 条"
402
+ if commit[:summary] && commit[:summary][:triggered]
403
+ puts " - 总结更新: #{commit[:summary][:trigger_reason]}"
404
+ end
405
+ end
406
+
407
+ # =============================================================================
408
+ # 步骤 6: 展示对话总结
409
+ # =============================================================================
410
+
411
+ puts "\n" + "=" * 80
412
+ puts "对话总结"
413
+ puts "=" * 80
414
+
415
+ diagnostics = SmartBrain.diagnostics
416
+
417
+ # 获取 Working Summary
418
+ final_summary = diagnostics.dig(:summaries, session_id)
419
+ if final_summary
420
+ puts "\n📝 Working Summary:"
421
+ puts final_summary[:text] if final_summary[:text]
422
+ end
423
+
424
+ # 统计信息
425
+ session_turns = diagnostics[:turns]&.select { |t| t[:session_id] == session_id } || []
426
+ puts "\n📊 统计信息:"
427
+ puts " - 总轮数: #{session_turns.size}"
428
+ puts " - Session ID: #{session_id}"
429
+
430
+ # 检索到的资源证据
431
+ resource_evidence_count = session_turns.sum do |turn|
432
+ (turn[:context]&.dig(:evidence) || []).count { |e| e[:source] == 'resource' }
433
+ end
434
+ puts " - 资源证据检索次数: #{resource_evidence_count}"
435
+
436
+ puts "\n" + "=" * 80
437
+ puts "演示结束!"
438
+ puts "=" * 80
@@ -0,0 +1,98 @@
1
+ -- SmartBrain v0.1 initial schema (PostgreSQL)
2
+
3
+ CREATE TABLE IF NOT EXISTS sessions (
4
+ id TEXT PRIMARY KEY,
5
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
6
+ metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb
7
+ );
8
+
9
+ CREATE TABLE IF NOT EXISTS turns (
10
+ id TEXT PRIMARY KEY,
11
+ session_id TEXT NOT NULL REFERENCES sessions(id),
12
+ seq INTEGER NOT NULL,
13
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
14
+ );
15
+
16
+ CREATE TABLE IF NOT EXISTS messages (
17
+ id TEXT PRIMARY KEY,
18
+ turn_id TEXT NOT NULL REFERENCES turns(id),
19
+ role TEXT NOT NULL,
20
+ content TEXT NOT NULL,
21
+ model TEXT,
22
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
23
+ meta_json JSONB NOT NULL DEFAULT '{}'::jsonb
24
+ );
25
+
26
+ CREATE TABLE IF NOT EXISTS tool_calls (
27
+ id TEXT PRIMARY KEY,
28
+ turn_id TEXT NOT NULL REFERENCES turns(id),
29
+ name TEXT NOT NULL,
30
+ args_json JSONB NOT NULL DEFAULT '{}'::jsonb,
31
+ result_json JSONB NOT NULL DEFAULT '{}'::jsonb,
32
+ status TEXT NOT NULL,
33
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS refs (
37
+ id TEXT PRIMARY KEY,
38
+ turn_id TEXT NOT NULL REFERENCES turns(id),
39
+ ref_type TEXT NOT NULL,
40
+ ref_uri TEXT NOT NULL,
41
+ ref_meta_json JSONB NOT NULL DEFAULT '{}'::jsonb,
42
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS memory_items (
46
+ id TEXT PRIMARY KEY,
47
+ session_id TEXT NOT NULL,
48
+ type TEXT NOT NULL,
49
+ key TEXT NOT NULL,
50
+ value_json JSONB NOT NULL,
51
+ confidence NUMERIC(3,2) NOT NULL DEFAULT 0.6,
52
+ status TEXT NOT NULL DEFAULT 'active',
53
+ source_turn_id TEXT,
54
+ source_message_id TEXT,
55
+ evidence_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
56
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
57
+ );
58
+
59
+ CREATE TABLE IF NOT EXISTS memory_chunks (
60
+ id TEXT PRIMARY KEY,
61
+ memory_item_id TEXT NOT NULL REFERENCES memory_items(id),
62
+ text TEXT NOT NULL,
63
+ tsv TSVECTOR,
64
+ meta_json JSONB NOT NULL DEFAULT '{}'::jsonb
65
+ );
66
+
67
+ CREATE TABLE IF NOT EXISTS entities (
68
+ id TEXT PRIMARY KEY,
69
+ name TEXT NOT NULL,
70
+ kind TEXT NOT NULL,
71
+ canonical_id TEXT
72
+ );
73
+
74
+ CREATE TABLE IF NOT EXISTS entity_mentions (
75
+ id TEXT PRIMARY KEY,
76
+ entity_id TEXT NOT NULL REFERENCES entities(id),
77
+ turn_id TEXT,
78
+ message_id TEXT,
79
+ span_json JSONB NOT NULL DEFAULT '{}'::jsonb,
80
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
81
+ );
82
+
83
+ CREATE TABLE IF NOT EXISTS summaries (
84
+ session_id TEXT PRIMARY KEY,
85
+ summary_text TEXT NOT NULL,
86
+ summary_version INTEGER NOT NULL DEFAULT 1,
87
+ summary_source_turn_range JSONB NOT NULL DEFAULT '{}'::jsonb,
88
+ summary_generated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
89
+ );
90
+
91
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_turns_session_seq ON turns(session_id, seq);
92
+ CREATE INDEX IF NOT EXISTS idx_memory_items_session_status ON memory_items(session_id, status);
93
+ CREATE INDEX IF NOT EXISTS idx_memory_items_type_key ON memory_items(type, key);
94
+ CREATE INDEX IF NOT EXISTS idx_messages_turn_id ON messages(turn_id);
95
+ CREATE INDEX IF NOT EXISTS idx_refs_turn_id ON refs(turn_id);
96
+ CREATE INDEX IF NOT EXISTS idx_entity_mentions_entity_id ON entity_mentions(entity_id);
97
+
98
+ CREATE INDEX IF NOT EXISTS idx_memory_chunks_tsv ON memory_chunks USING GIN (tsv);
data/example.rb ADDED
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'json'
5
+
6
+ # Follow smart_agent/test.rb style: load SmartPrompt first, then SmartAgent.
7
+ require 'smart_prompt'
8
+ require 'smart_agent'
9
+ require_relative 'lib/smart_brain'
10
+ require_relative 'lib/smart_brain/adapters/smart_rag/direct_client'
11
+
12
+ begin
13
+ require '/home/mlf/smart_ai/smart_rag/lib/smart_rag'
14
+ rescue LoadError => e
15
+ warn "SmartRAG load failed: #{e.message}"
16
+ warn 'Please install SmartRAG dependencies (especially sequel/pg) before running this example.'
17
+ exit 1
18
+ end
19
+
20
+ rag_config = SmartRAG::Config.load('/home/mlf/smart_ai/smart_rag/config/smart_rag.yml')
21
+ # SmartRAG config may include database.extensions with pgvector.
22
+ # For Sequel, pgvector should be loaded globally via Sequel.extension.
23
+ db_cfg = (rag_config[:database] || {}).dup
24
+ db_exts = Array(db_cfg.delete(:extensions)).map(&:to_s)
25
+ if db_exts.include?('pgvector')
26
+ require 'sequel'
27
+ Sequel.extension 'pgvector'
28
+ end
29
+ # SmartRAG currently passes database config into EmbeddingService.
30
+ # Inject SmartPrompt config path here so EmbeddingService can boot correctly.
31
+ db_cfg[:config_path] = File.expand_path('./config/example_llm.yml', __dir__)
32
+ rag_config = rag_config.merge(database: db_cfg)
33
+
34
+ rag = SmartRAG::SmartRAG.new(rag_config)
35
+ rag_client = SmartBrain::Adapters::SmartRag::DirectClient.new(rag: rag)
36
+
37
+ SmartBrain.configure(smart_rag_client: rag_client)
38
+ engine = SmartAgent::Engine.new('./config/example_agent.yml')
39
+ agent = engine.build_agent(:brain_assistant)
40
+
41
+ session_id = 'smartagent-smartbrain-demo'
42
+ user_messages = [
43
+ '请记住:SmartRAG 作为 SmartBrain 的底层基础库,提供存储与检索服务',
44
+ '继续这个话题,SmartBrain 默认使用哪种数据库?'
45
+ ]
46
+
47
+ def build_worker_input(context, user_message)
48
+ {
49
+ context_id: context[:context_id],
50
+ working_summary: context[:working_summary],
51
+ recent_turns: context[:recent_turns],
52
+ evidence: context[:evidence],
53
+ latest_user_message: user_message,
54
+ constraints: context[:constraints]
55
+ }.to_json
56
+ end
57
+
58
+ user_messages.each_with_index do |user_message, idx|
59
+ # 1) SmartBrain composes context (will call SmartRAG when planner enables resource retrieval).
60
+ context = SmartBrain.compose_context(
61
+ session_id: session_id,
62
+ user_message: user_message,
63
+ agent_state: { agent: 'SmartAgent', turn: idx + 1 }
64
+ )
65
+
66
+ # 2) SmartAgent executes the real call_worker flow.
67
+ assistant_message = agent.please(build_worker_input(context, user_message))
68
+
69
+ # 3) SmartBrain commits this turn.
70
+ commit = SmartBrain.commit_turn(
71
+ session_id: session_id,
72
+ turn_events: {
73
+ messages: [
74
+ { role: 'user', content: user_message },
75
+ { role: 'assistant', content: assistant_message.to_s }
76
+ ],
77
+ decisions: (idx.zero? ? [{ key: 'decision:smartbrain:storage', decision: 'Use Postgres by default' }] : [])
78
+ }
79
+ )
80
+
81
+ resource_hits = Array(context[:evidence]).count { |e| e[:source] == 'resource' }
82
+ memory_hits = Array(context[:evidence]).count { |e| e[:source] == 'memory' }
83
+
84
+ puts "\n=== Turn #{idx + 1} ==="
85
+ puts "context_id: #{context[:context_id]}"
86
+ puts "request_id: #{context.dig(:debug, :trace, :request_id)}"
87
+ puts "plan_id: #{context.dig(:debug, :trace, :plan_id)}"
88
+ puts "commit_id: #{commit[:commit_id]}"
89
+ puts "evidence(memory/resource): #{memory_hits}/#{resource_hits}"
90
+ puts "assistant: #{assistant_message}"
91
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartBrain
4
+ module Adapters
5
+ module SmartRag
6
+ class DirectClient
7
+ def initialize(rag:)
8
+ @rag = rag
9
+ end
10
+
11
+ def retrieve(plan)
12
+ response = rag.retrieve(plan: plan)
13
+ normalize_pack(response, request_id: plan[:request_id])
14
+ rescue StandardError => e
15
+ {
16
+ version: '0.1',
17
+ request_id: plan[:request_id],
18
+ plan_id: "direct-error-#{plan[:request_id]}",
19
+ generated_at: Time.now.utc.iso8601,
20
+ evidences: [],
21
+ stats: { candidates: 0, returned: 0, took_ms: 0 },
22
+ explain: { ignored_fields: [] },
23
+ warnings: ["smart_rag direct retrieve failed: #{e.message}"]
24
+ }
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :rag
30
+
31
+ def normalize_pack(response, request_id:)
32
+ pack = response.is_a?(Hash) ? response : {}
33
+ {
34
+ version: pack[:version] || '0.1',
35
+ request_id: pack[:request_id] || request_id,
36
+ plan_id: pack[:plan_id] || "direct-#{request_id}",
37
+ generated_at: pack[:generated_at] || Time.now.utc.iso8601,
38
+ evidences: Array(pack[:evidences]),
39
+ stats: pack[:stats] || { candidates: Array(pack[:evidences]).size, returned: Array(pack[:evidences]).size, took_ms: 0 },
40
+ explain: pack[:explain] || { ignored_fields: [] },
41
+ warnings: Array(pack[:warnings])
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end