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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.en.md +173 -0
- data/README.md +173 -0
- data/agents/brain_assistant.rb +11 -0
- data/config/brain.yml +32 -0
- data/conversation_demo.rb +438 -0
- data/db/migrate/001_init.sql +98 -0
- data/example.rb +91 -0
- data/lib/smart_brain/adapters/smart_rag/direct_client.rb +47 -0
- data/lib/smart_brain/adapters/smart_rag/http_client.rb +61 -0
- data/lib/smart_brain/adapters/smart_rag/null_client.rb +22 -0
- data/lib/smart_brain/configuration.rb +41 -0
- data/lib/smart_brain/consolidator/working_summary.rb +102 -0
- data/lib/smart_brain/context_composer/composer.rb +75 -0
- data/lib/smart_brain/contracts/context_package.rb +16 -0
- data/lib/smart_brain/contracts/evidence_pack.rb +16 -0
- data/lib/smart_brain/contracts/retrieval_plan.rb +17 -0
- data/lib/smart_brain/event_store/in_memory.rb +103 -0
- data/lib/smart_brain/fusion/merger.rb +137 -0
- data/lib/smart_brain/memory_extractor/extractor.rb +92 -0
- data/lib/smart_brain/memory_store/in_memory.rb +78 -0
- data/lib/smart_brain/observability/tracker.rb +60 -0
- data/lib/smart_brain/retrieval_planner/planner.rb +122 -0
- data/lib/smart_brain/retrievers/exact_retriever.rb +62 -0
- data/lib/smart_brain/retrievers/memory_retriever.rb +30 -0
- data/lib/smart_brain/retrievers/relational_retriever.rb +53 -0
- data/lib/smart_brain/runtime.rb +195 -0
- data/lib/smart_brain/version.rb +5 -0
- data/lib/smart_brain.rb +35 -0
- data/templates/brain_assistant.erb +5 -0
- data/workers/brain_assistant.rb +9 -0
- 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
|