smart_brain 0.1.1 → 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 +2 -2
- data/README.md +356 -62
- data/config/brain.yml +69 -1
- data/conversation_demo.rb +5 -5
- 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 +2 -2
- 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 +1 -1
- data/lib/smart_brain.rb +49 -4
- metadata +88 -36
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
#
|
|
4
|
+
# 场景三:Postgres 持久化 —— 跨「重启」召回。
|
|
5
|
+
# 演示:进程 A 写记忆 → 进程 B(新 Runtime,同一 DB)经 FTS 召回。
|
|
6
|
+
#
|
|
7
|
+
# 前置:本地 PG + smart_brain 角色/库。若没建,脚本会打印步骤并退出。
|
|
8
|
+
# sudo -u postgres createuser -d smart_brain
|
|
9
|
+
# sudo -u postgres psql -c "ALTER USER smart_brain PASSWORD 'smart_brain';"
|
|
10
|
+
# sudo -u postgres createdb -O smart_brain smart_brain_test
|
|
11
|
+
#
|
|
12
|
+
# 运行: SMARTBRAIN_DB_NAME=smart_brain_test ruby examples/03_postgres_persistence.rb
|
|
13
|
+
|
|
14
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
15
|
+
require 'smart_brain'
|
|
16
|
+
require 'smart_brain/db'
|
|
17
|
+
|
|
18
|
+
def hr(title) = puts("\n=== #{title} ===")
|
|
19
|
+
|
|
20
|
+
ENV['SMARTBRAIN_BACKEND'] = 'postgres'
|
|
21
|
+
ENV['SMARTBRAIN_DB_NAME'] ||= 'smart_brain_test'
|
|
22
|
+
|
|
23
|
+
config = SmartBrain::Configuration.load
|
|
24
|
+
unless SmartBrain::DB.reachable?(config)
|
|
25
|
+
warn "✗ 连不上 Postgres(#{config.database_config[:host]}:#{config.database_config[:port]}/#{config.database_config[:database]})。"
|
|
26
|
+
warn " 先建库:见脚本顶部注释或 docs/user_guide.md §10。"
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 清场(保证可重复运行)
|
|
31
|
+
db = SmartBrain::DB.connect(config)
|
|
32
|
+
SmartBrain::DB.migrate(db)
|
|
33
|
+
db.run('TRUNCATE kg_edges, knowledge_events, memory_chunks, memory_items, summaries, messages, ' \
|
|
34
|
+
'refs, tool_calls, turns, sessions, entities, entity_mentions RESTART IDENTITY CASCADE')
|
|
35
|
+
SmartBrain::DB.disconnect!
|
|
36
|
+
|
|
37
|
+
sid = 'pg-demo'
|
|
38
|
+
|
|
39
|
+
hr '1) 进程 A:commit_turn 写入(含中文决策 + KG 边)'
|
|
40
|
+
SmartBrain.configure
|
|
41
|
+
SmartBrain.commit_turn(
|
|
42
|
+
session_id: sid,
|
|
43
|
+
turn_events: {
|
|
44
|
+
messages: [{ role: 'user', content: '记一下:SmartBrain 用 Postgres 做持久化' }],
|
|
45
|
+
decisions: [{ key: 'decision:pg:persist', decision: 'SmartBrain 用 Postgres 持久化记忆' }],
|
|
46
|
+
entities: [{ key: 'entity:db:pg', name: 'Postgres', canonical: 'postgres', remember: true }],
|
|
47
|
+
edges: [{ subject: 'smart_brain', predicate: 'persists_via', object: 'postgres' }]
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
puts '✓ 进程 A 写完,退出(模拟进程结束)'
|
|
51
|
+
|
|
52
|
+
hr '2) 进程 B:全新 Runtime(同一 DB)—— 验证持久化'
|
|
53
|
+
SmartBrain.configure # 模拟重启:构造全新的 Runtime,共享同一个 Postgres
|
|
54
|
+
puts "backend=#{SmartBrain.storage_backend}"
|
|
55
|
+
hits = SmartBrain.search_memory(session_id: sid, query: '持久化')
|
|
56
|
+
puts "FTS 搜 '持久化' → #{hits.size} 条(mode=#{hits.first&.dig(:mode)})"
|
|
57
|
+
puts "kg_query → #{SmartBrain.kg_query(session_id: sid, subject: 'smart_brain').map { |e| "#{e[:predicate]} #{e[:object]}" }.inspect}"
|
|
58
|
+
|
|
59
|
+
ctx = SmartBrain.compose_context(session_id: sid, user_message: '持久化方案是什么?')
|
|
60
|
+
mem = ctx[:evidence].select { |e| e[:source] == 'memory' }
|
|
61
|
+
puts "compose_context 召回 memory 证据 #{mem.size} 条(跨重启仍命中)"
|
|
62
|
+
|
|
63
|
+
puts "\n✅ 场景三完成。记忆、KG 边、摘要均跨进程持久化。"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
#
|
|
4
|
+
# 场景四:Ollama LLM —— 真摘要 + rerank。
|
|
5
|
+
# 演示 ModelProvider 接真实本地模型后:working_summary 走真摘要、Fusion 走 LLM rerank。
|
|
6
|
+
#
|
|
7
|
+
# 前置:本机 ollama 已起、装了某模型(默认用 llama2 —— 快、非思维链;qwen3 更强但慢)。
|
|
8
|
+
# ollama serve & ollama pull llama2
|
|
9
|
+
#
|
|
10
|
+
# 运行: ruby examples/04_ollama_llm.rb
|
|
11
|
+
# 换模型: SMARTBRAIN_LLM_MODEL=qwen3 ruby examples/04_ollama_llm.rb
|
|
12
|
+
|
|
13
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
14
|
+
require 'smart_brain'
|
|
15
|
+
require 'smart_brain/model_provider'
|
|
16
|
+
require 'smart_brain/memory_store/in_memory'
|
|
17
|
+
require 'smart_brain/consolidator/working_summary'
|
|
18
|
+
|
|
19
|
+
def hr(title) = puts("\n=== #{title} ===")
|
|
20
|
+
|
|
21
|
+
base_url = ENV.fetch('SMARTBRAIN_LLM_BASE_URL', 'http://localhost:11434')
|
|
22
|
+
model = ENV.fetch('SMARTBRAIN_LLM_MODEL', 'llama2')
|
|
23
|
+
|
|
24
|
+
unless system("curl -s -m 2 #{base_url}/api/tags >/dev/null 2>&1")
|
|
25
|
+
warn "✗ 连不上 ollama(#{base_url})。先 ollama serve 并 ollama pull #{model}。"
|
|
26
|
+
exit 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
provider = SmartBrain::ModelProvider::Ollama.new(
|
|
30
|
+
model: model, base_url: base_url, temperature: 0.2, timeout_seconds: 180, think: false
|
|
31
|
+
)
|
|
32
|
+
puts "provider=#{provider.class} model=#{model} llm?=#{provider.llm?}"
|
|
33
|
+
|
|
34
|
+
hr '1) complete:让模型回答'
|
|
35
|
+
r = provider.complete(prompt: '用一句话说明 PostgreSQL 的 FTS。', max_tokens: 120)
|
|
36
|
+
puts "回答:#{r[:text].strip[0, 200]}"
|
|
37
|
+
|
|
38
|
+
hr '2) rerank:LLM-as-judge 重排候选'
|
|
39
|
+
docs = [
|
|
40
|
+
'埃菲尔铁塔在巴黎。',
|
|
41
|
+
'PostgreSQL 支持 JSONB 与全文检索(FTS)。',
|
|
42
|
+
'Ruby 是一种动态语言。'
|
|
43
|
+
]
|
|
44
|
+
ranked = provider.rerank(query: '关系型数据库的全文检索', documents: docs)
|
|
45
|
+
puts '按相关性重排:'
|
|
46
|
+
ranked.each { |x| puts " [#{x[:index]}] score=#{x[:score].round(2)} :: #{docs[x[:index]][0, 40]}" }
|
|
47
|
+
|
|
48
|
+
hr '3) working_summary:真摘要(带模板兜底)'
|
|
49
|
+
store = SmartBrain::MemoryStore::InMemory.new
|
|
50
|
+
store.upsert(session_id: 's', items: [
|
|
51
|
+
{ type: 'decisions', key: 'decision:db', value_json: { decision: '用 Postgres 持久化' },
|
|
52
|
+
confidence: 0.8, status: 'active', tier: 'dao_ren', lifecycle_status: 'raw', updated_at: Time.now.utc.iso8601 },
|
|
53
|
+
{ type: 'tasks', key: 'task:mcp', value_json: { title: '加 MCP server' },
|
|
54
|
+
confidence: 0.9, status: 'active', tier: 'shu', lifecycle_status: 'raw', updated_at: Time.now.utc.iso8601 }
|
|
55
|
+
])
|
|
56
|
+
ws = SmartBrain::Consolidator::WorkingSummary.new(
|
|
57
|
+
config: SmartBrain::Configuration.load, clock: -> { Time.now.utc },
|
|
58
|
+
memory_store: store, model_provider: provider
|
|
59
|
+
)
|
|
60
|
+
summary = ws.update(
|
|
61
|
+
session_id: 's', turn_count: 1, stage_event: true,
|
|
62
|
+
recent_turns: [{ role: 'user', content: '用 Postgres' }, { role: 'assistant', content: '好,持久化到 memory_chunks' }],
|
|
63
|
+
memory_items: store.active_items(session_id: 's')
|
|
64
|
+
)
|
|
65
|
+
puts "summary_method=#{summary[:summary_method]}"
|
|
66
|
+
puts "摘要:\n#{summary[:text]}"
|
|
67
|
+
|
|
68
|
+
puts "\n✅ 场景四完成。stub→ollama 后 summary/rerank 走真实模型(超时/截断自动降级到模板/词法)。"
|
|
69
|
+
puts " 注:思维链模型(qwen3/deepseek-r1)同步较慢,本示例默认 llama2;换 qwen3 用 SMARTBRAIN_LLM_MODEL=qwen3。"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
#
|
|
4
|
+
# 场景五:SmartBrain + SmartRAG —— 对话记忆 + 资源检索融合。
|
|
5
|
+
# SmartBrain 管对话记忆/治理,SmartRAG 管文档/网页 RAG;compose_context 命中「查资料/引用」
|
|
6
|
+
# 意图时自动调用 SmartRAG,再由 Fusion 统一去重 + rerank + diversity。
|
|
7
|
+
#
|
|
8
|
+
# 前置(缺任一则脚本会打印步骤并优雅退出):
|
|
9
|
+
# 1) SmartRAG gem 已装(bundle 里已有 smart_rag)。
|
|
10
|
+
# 2) SmartRAG 的数据库可连(默认读下方 env)。
|
|
11
|
+
# 3) (可选)embedding/llm 配置。
|
|
12
|
+
# 完整可运行版见仓库根目录 conversation_demo.rb。
|
|
13
|
+
#
|
|
14
|
+
# 运行:
|
|
15
|
+
# SMARTBRAIN_RAG_DB_HOST=127.0.0.1 \
|
|
16
|
+
# SMARTBRAIN_RAG_DB_NAME=smart_rag_development \
|
|
17
|
+
# SMARTBRAIN_RAG_DB_USER=rag_user SMARTBRAIN_RAG_DB_PASSWORD=rag_pwd \
|
|
18
|
+
# ruby examples/05_smart_rag_integration.rb
|
|
19
|
+
|
|
20
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
21
|
+
require 'smart_brain'
|
|
22
|
+
require 'smart_brain/adapters/smart_rag/direct_client'
|
|
23
|
+
|
|
24
|
+
def hr(title) = puts("\n=== #{title} ===")
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
require 'smart_rag'
|
|
28
|
+
rescue LoadError => e
|
|
29
|
+
warn "✗ 加载 smart_rag 失败:#{e.message}"
|
|
30
|
+
warn " 请先确保 smart_rag gem 可用(bundle 里已声明)。"
|
|
31
|
+
exit 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
rag_config = {
|
|
35
|
+
database: {
|
|
36
|
+
adapter: 'postgresql',
|
|
37
|
+
host: ENV.fetch('SMARTBRAIN_RAG_DB_HOST', '127.0.0.1'),
|
|
38
|
+
port: ENV.fetch('SMARTBRAIN_RAG_DB_PORT', '5432').to_i,
|
|
39
|
+
database: ENV.fetch('SMARTBRAIN_RAG_DB_NAME', 'smart_rag_development'),
|
|
40
|
+
user: ENV.fetch('SMARTBRAIN_RAG_DB_USER', 'rag_user'),
|
|
41
|
+
password: ENV.fetch('SMARTBRAIN_RAG_DB_PASSWORD', 'rag_pwd')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
rag =
|
|
46
|
+
begin
|
|
47
|
+
SmartRAG::SmartRAG.new(rag_config)
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
warn "✗ SmartRAG 初始化失败:#{e.class}: #{e.message}"
|
|
50
|
+
warn " 检查上方数据库 env,或参考仓库根目录 conversation_demo.rb 的完整配置(含 llm/embedding)。"
|
|
51
|
+
exit 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# 挂载资源后端
|
|
55
|
+
SmartBrain.configure(smart_rag_client: SmartBrain::Adapters::SmartRag::DirectClient.new(rag: rag))
|
|
56
|
+
sid = "rag-#{Process.pid}"
|
|
57
|
+
|
|
58
|
+
hr '1) commit_turn:写一轮对话记忆'
|
|
59
|
+
SmartBrain.commit_turn(
|
|
60
|
+
session_id: sid,
|
|
61
|
+
turn_events: {
|
|
62
|
+
messages: [{ role: 'user', content: '帮我查一下 Ruby 命名规范的资料' }],
|
|
63
|
+
goals: [{ key: 'goal:learn:ruby', goal: '学习 Ruby 规范' }]
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
puts '✓ 对话记忆已写'
|
|
67
|
+
|
|
68
|
+
hr '2) compose_context:命中「查资料」意图 → 调 SmartRAG + 融合'
|
|
69
|
+
ctx = SmartBrain.compose_context(session_id: sid, user_message: '查一下 Ruby 命名规范并给出引用')
|
|
70
|
+
mem = ctx[:evidence].count { |e| e[:source] == 'memory' }
|
|
71
|
+
res = ctx[:evidence].count { |e| e[:source] == 'resource' }
|
|
72
|
+
puts "evidence:memory=#{mem} resource=#{res} (memory/resource 比 = #{SmartBrain.diagnostics[:metrics][:memory_resource_ratio]})"
|
|
73
|
+
puts "资源检索是否触发:#{ctx.dig(:debug, :planner, :purpose) == 'research' ? '是(purpose=research)' : '否'}"
|
|
74
|
+
ctx[:evidence].first(3).each do |e|
|
|
75
|
+
puts " - [#{e[:source]}] #{e[:title]} score=#{e[:score].round(2)}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
puts "\n✅ 场景五完成。SmartBrain 记忆 + SmartRAG 资源在 Fusion 层统一融合。"
|
|
79
|
+
puts " 若上方 resource=0:往 SmartRAG 灌点文档(rag.add_document ...),或换一个明显命中资源检索的 query。"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'smart_brain'
|
|
4
|
+
|
|
5
|
+
SmartBrain.configure
|
|
6
|
+
|
|
7
|
+
domain_id = 'demo-domain'
|
|
8
|
+
project_ref = { type: 'project', id: 'project-001' }
|
|
9
|
+
task_ref = { type: 'task', id: 'task-030' }
|
|
10
|
+
scope_context = {
|
|
11
|
+
read: [{ type: 'global', id: 'default' }, project_ref, task_ref],
|
|
12
|
+
write: [project_ref, task_ref],
|
|
13
|
+
default_write: task_ref
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
source = SmartBrain.commit_turn(
|
|
17
|
+
domain_id: domain_id,
|
|
18
|
+
session_id: 'writer-session',
|
|
19
|
+
scope_context: scope_context,
|
|
20
|
+
turn_events: {
|
|
21
|
+
decisions: [{ key: 'decision:database', decision: 'Use PostgreSQL for this task' }]
|
|
22
|
+
}
|
|
23
|
+
).dig(:memory_written, :items).first
|
|
24
|
+
|
|
25
|
+
promotion = SmartBrain.promote_to_scope(
|
|
26
|
+
memory_item_id: source[:id],
|
|
27
|
+
target_scope: project_ref,
|
|
28
|
+
domain_id: domain_id,
|
|
29
|
+
session_id: 'writer-session',
|
|
30
|
+
scope_context: scope_context,
|
|
31
|
+
verification_refs: ['architecture-review-001'],
|
|
32
|
+
reason: 'Confirmed as a project-wide decision',
|
|
33
|
+
reviewer: 'human'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
reader_context = {
|
|
37
|
+
read: [{ type: 'global', id: 'default' }, project_ref, { type: 'task', id: 'task-031' }],
|
|
38
|
+
write: [{ type: 'task', id: 'task-031' }],
|
|
39
|
+
default_write: { type: 'task', id: 'task-031' }
|
|
40
|
+
}
|
|
41
|
+
context = SmartBrain.compose_context(
|
|
42
|
+
domain_id: domain_id,
|
|
43
|
+
session_id: 'reader-session',
|
|
44
|
+
scope_context: reader_context,
|
|
45
|
+
user_message: 'Which database did the project choose?'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
puts "Promoted item: #{promotion.dig(:memory_item, :id)}"
|
|
49
|
+
puts "Recalled scopes: #{context[:evidence].map { |item| item[:scope] }.compact.inspect}"
|
|
50
|
+
puts "Lineage: #{SmartBrain.promotion_lineage(memory_item_id: promotion.dig(:memory_item, :id)).map { |item| item[:id] }.inspect}"
|
data/examples/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# SmartBrain 示例集
|
|
2
|
+
|
|
3
|
+
每个脚本都是**自包含、可独立运行**的,从易到难、按需引入外部依赖。
|
|
4
|
+
全部默认零外部依赖即可起步;PG / Ollama / SmartRAG 按场景按需开启。
|
|
5
|
+
|
|
6
|
+
## 场景速查
|
|
7
|
+
|
|
8
|
+
| 脚本 | 场景 | 依赖 | 后端 |
|
|
9
|
+
|---|---|---|---|
|
|
10
|
+
| [`01_memory_basic.rb`](01_memory_basic.rb) | 最小闭环:commit→compose→search→brief→diagnostics | 无 | memory |
|
|
11
|
+
| [`02_governance.rb`](02_governance.rb) | 记忆治理:lifecycle + KG + fact_check + brief | 无 | memory |
|
|
12
|
+
| [`03_postgres_persistence.rb`](03_postgres_persistence.rb) | 跨「重启」持久化召回 | PostgreSQL | postgres |
|
|
13
|
+
| [`04_ollama_llm.rb`](04_ollama_llm.rb) | LLM 真摘要 + rerank | Ollama | memory |
|
|
14
|
+
| [`05_smart_rag_integration.rb`](05_smart_rag_integration.rb) | 对话记忆 + 资源 RAG 融合 | SmartRAG | memory |
|
|
15
|
+
| [`06_multi_scope_memory.rb`](06_multi_scope_memory.rb) | project/task 跨会话共享、冲突与 promotion | 无 | memory |
|
|
16
|
+
|
|
17
|
+
## 运行
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cd /root/smart_brain
|
|
21
|
+
|
|
22
|
+
ruby examples/01_memory_basic.rb # 直接跑,无需任何配置
|
|
23
|
+
ruby examples/02_governance.rb # 直接跑
|
|
24
|
+
|
|
25
|
+
SMARTBRAIN_DB_NAME=smart_brain_test \
|
|
26
|
+
ruby examples/03_postgres_persistence.rb # 需先建库(见脚本顶部注释)
|
|
27
|
+
|
|
28
|
+
ruby examples/04_ollama_llm.rb # 需 ollama serve + ollama pull llama2
|
|
29
|
+
SMARTBRAIN_LLM_MODEL=qwen3 ruby examples/04_ollama_llm.rb # 换更强的模型(更慢)
|
|
30
|
+
|
|
31
|
+
SMARTBRAIN_RAG_DB_HOST=... SMARTBRAIN_RAG_DB_NAME=smart_rag_development \
|
|
32
|
+
ruby examples/05_smart_rag_integration.rb # 需 SmartRAG 实例
|
|
33
|
+
ruby examples/06_multi_scope_memory.rb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> 没装对应依赖时,03/04/05 会**打印设置步骤并优雅退出**(不会崩)。
|
|
37
|
+
|
|
38
|
+
## 阅读顺序建议
|
|
39
|
+
|
|
40
|
+
1. `01_memory_basic.rb` —— 先建立 commit/compose 心智模型。
|
|
41
|
+
2. `02_governance.rb` —— 理解「记忆宫殿」的分层与生命周期。
|
|
42
|
+
3. 按需挑 03/04/05 之一看持久化、LLM、资源检索如何接入。
|
|
43
|
+
|
|
44
|
+
## 参考文档
|
|
45
|
+
|
|
46
|
+
- 完整用法:[`../docs/user_guide.md`](../docs/user_guide.md)
|
|
47
|
+
- MCP 接入:[`../docs/mcp.md`](../docs/mcp.md)
|
|
48
|
+
- 设计与契约:`../docs/smartbrain_design.md`、`../docs/memory_types.md`、`../docs/policies.md`
|
|
49
|
+
- 完整 SmartRAG 集成 demo:`../conversation_demo.rb`、`../example.rb`
|
data/exe/smart_brain
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# SmartBrain CLI — drive the memory runtime from the shell or as an agent tool.
|
|
5
|
+
#
|
|
6
|
+
# smart_brain status
|
|
7
|
+
# smart_brain migrate (postgres backend only)
|
|
8
|
+
# smart_brain commit --data '{"session_id":..,"turn_events":{..}}'
|
|
9
|
+
# smart_brain compose --data '{"session_id":..,"user_message":..}'
|
|
10
|
+
# smart_brain search --data '{"session_id":..,"query":..}'
|
|
11
|
+
# smart_brain serve [--host 0.0.0.0] [--port 9292] [--config PATH]
|
|
12
|
+
# smart_brain mcp [--config PATH] (stdio JSON-RPC, for Claude Code / Cursor)
|
|
13
|
+
|
|
14
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
15
|
+
|
|
16
|
+
require 'optparse'
|
|
17
|
+
require 'json'
|
|
18
|
+
require 'smart_brain'
|
|
19
|
+
require 'smart_brain/server/service'
|
|
20
|
+
require 'smart_brain/server/http_app'
|
|
21
|
+
require 'smart_brain/server/mcp_server'
|
|
22
|
+
|
|
23
|
+
module SmartBrain
|
|
24
|
+
module CLI
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
def run(argv)
|
|
28
|
+
config_path = ENV['SMARTBRAIN_CONFIG']
|
|
29
|
+
global = OptionParser.new do |opts|
|
|
30
|
+
opts.banner = 'Usage: smart_brain <command> [options]'
|
|
31
|
+
opts.on('--config PATH', 'Path to brain.yml') { |p| config_path = p }
|
|
32
|
+
opts.on('--version', 'Print version and exit') { puts SmartBrain::VERSION; exit 0 }
|
|
33
|
+
opts.on('-h', '--help', 'Show this help') { puts opts; exit 0 }
|
|
34
|
+
end
|
|
35
|
+
global.order!(argv)
|
|
36
|
+
|
|
37
|
+
command = argv.shift
|
|
38
|
+
abort(global.to_s) unless command
|
|
39
|
+
|
|
40
|
+
case command
|
|
41
|
+
when 'status' then print_json(Server::Service.build(config_path: config_path).status)
|
|
42
|
+
when 'migrate' then print_json(ok: Server::Service.build(config_path: config_path).migrate)
|
|
43
|
+
when 'commit', 'compose', 'search', 'fact-check', 'brief', 'wake-up'
|
|
44
|
+
data_cmd(command, argv, config_path)
|
|
45
|
+
when 'knowledge' then knowledge_cmd(argv, config_path)
|
|
46
|
+
when 'kg' then kg_cmd(argv, config_path)
|
|
47
|
+
when 'serve' then serve(argv, config_path)
|
|
48
|
+
when 'mcp' then mcp(config_path)
|
|
49
|
+
else
|
|
50
|
+
warn "unknown command: #{command}"
|
|
51
|
+
warn global.to_s
|
|
52
|
+
exit 1
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def data_cmd(command, argv, config_path)
|
|
57
|
+
options = {}
|
|
58
|
+
parser = OptionParser.new do |opts|
|
|
59
|
+
opts.on('--data JSON', 'JSON payload (or read from stdin)') { |j| options[:data] = j }
|
|
60
|
+
opts.on('-h', '--help') { puts opts; exit 0 }
|
|
61
|
+
end
|
|
62
|
+
parser.parse!(argv)
|
|
63
|
+
|
|
64
|
+
payload = read_payload(options[:data])
|
|
65
|
+
service = Server::Service.build(config_path: config_path)
|
|
66
|
+
result =
|
|
67
|
+
case command
|
|
68
|
+
when 'commit' then service.commit(**payload.slice(:domain_id, :session_id, :scope_context, :turn_events).merge(turn_events: payload[:turn_events] || {}))
|
|
69
|
+
when 'compose' then service.compose(**payload.slice(:domain_id, :session_id, :scope_context, :user_message, :agent_state).merge(agent_state: payload[:agent_state] || {}))
|
|
70
|
+
when 'search' then service.search(**payload.slice(:domain_id, :session_id, :scope_context, :query, :limit))
|
|
71
|
+
when 'fact-check' then service.fact_check(**payload.slice(:domain_id, :session_id, :scope_context, :text))
|
|
72
|
+
when 'brief' then service.brief(**payload.slice(:domain_id, :session_id, :scope_context, :query))
|
|
73
|
+
when 'wake-up' then service.wake_up(**payload.slice(:domain_id, :session_id, :scope_context))
|
|
74
|
+
end
|
|
75
|
+
print_json(result)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def knowledge_cmd(argv, config_path)
|
|
79
|
+
options = {}
|
|
80
|
+
OptionParser.new do |opts|
|
|
81
|
+
opts.banner = 'usage: smart_brain knowledge <distill|gate|promote|demote|retract|promote-to-scope|lineage|events> --data JSON'
|
|
82
|
+
opts.on('--data JSON', 'JSON payload (or read from stdin)') { |j| options[:data] = j }
|
|
83
|
+
opts.on('-h', '--help') { puts opts; exit 0 }
|
|
84
|
+
end.parse!(argv)
|
|
85
|
+
|
|
86
|
+
action = argv.shift
|
|
87
|
+
abort 'usage: smart_brain knowledge <distill|gate|promote|demote|retract|promote-to-scope|lineage|events> --data JSON' unless action
|
|
88
|
+
|
|
89
|
+
payload = read_payload(options[:data])
|
|
90
|
+
service = Server::Service.build(config_path: config_path)
|
|
91
|
+
result =
|
|
92
|
+
case action
|
|
93
|
+
when 'distill' then service.distill(**payload.slice(:domain_id, :session_id, :scope_context, :statement, :content, :tier, :supporting_refs, :domain, :field, :reviewer, :key))
|
|
94
|
+
when 'gate' then service.gate(**payload.slice(:memory_item_id, :domain_id, :session_id, :scope_context))
|
|
95
|
+
when 'promote' then service.promote(**payload.slice(:memory_item_id, :domain_id, :session_id, :scope_context, :verification_refs, :reason, :reviewer, :force))
|
|
96
|
+
when 'demote' then service.demote(**payload.slice(:memory_item_id, :domain_id, :session_id, :scope_context, :evidence_refs, :reason, :reason_type, :reviewer))
|
|
97
|
+
when 'retract' then service.retract(**payload.slice(:memory_item_id, :domain_id, :session_id, :scope_context, :evidence_refs, :reason, :reviewer))
|
|
98
|
+
when 'promote-to-scope' then service.promote_to_scope(**payload.slice(:memory_item_id, :target_scope, :domain_id, :session_id, :scope_context, :verification_refs, :reason, :reviewer))
|
|
99
|
+
when 'lineage' then service.promotion_lineage(**payload.slice(:memory_item_id, :domain_id, :session_id, :scope_context))
|
|
100
|
+
when 'events' then service.knowledge_events(**payload.slice(:memory_item_id, :domain_id, :session_id, :scope_context))
|
|
101
|
+
else abort "unknown knowledge action: #{action}"
|
|
102
|
+
end
|
|
103
|
+
print_json(result)
|
|
104
|
+
rescue SmartBrain::Governance::LifecycleGateError, ArgumentError => e
|
|
105
|
+
warn "#{e.class}: #{e.message}"
|
|
106
|
+
exit 1
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def kg_cmd(argv, config_path)
|
|
110
|
+
options = {}
|
|
111
|
+
OptionParser.new do |opts|
|
|
112
|
+
opts.banner = 'usage: smart_brain kg <add|query|timeline|invalidate|stats> --data JSON'
|
|
113
|
+
opts.on('--data JSON', 'JSON payload (or read from stdin)') { |j| options[:data] = j }
|
|
114
|
+
opts.on('-h', '--help') { puts opts; exit 0 }
|
|
115
|
+
end.parse!(argv)
|
|
116
|
+
action = argv.shift
|
|
117
|
+
abort 'usage: smart_brain kg <add|query|timeline|invalidate|stats> --data JSON' unless action
|
|
118
|
+
|
|
119
|
+
payload = read_payload(options[:data])
|
|
120
|
+
service = Server::Service.build(config_path: config_path)
|
|
121
|
+
result =
|
|
122
|
+
case action
|
|
123
|
+
when 'add' then service.kg_add(**payload.slice(:domain_id, :session_id, :scope_context, :scope_ref, :subject, :predicate, :object, :confidence, :source_turn_id))
|
|
124
|
+
when 'query' then service.kg_query(**payload.slice(:domain_id, :session_id, :scope_context, :subject, :predicate, :object, :include_invalid))
|
|
125
|
+
when 'timeline' then service.kg_timeline(**payload.slice(:domain_id, :session_id, :scope_context, :subject))
|
|
126
|
+
when 'invalidate' then service.kg_invalidate(**payload.slice(:edge_id, :reason, :domain_id, :session_id, :scope_context))
|
|
127
|
+
when 'stats' then service.kg_stats(**payload.slice(:domain_id, :session_id, :scope_context))
|
|
128
|
+
else abort "unknown kg action: #{action}"
|
|
129
|
+
end
|
|
130
|
+
print_json(result)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def serve(argv, config_path)
|
|
134
|
+
options = { host: '127.0.0.1', port: 9292 }
|
|
135
|
+
OptionParser.new do |opts|
|
|
136
|
+
opts.on('--host HOST') { |h| options[:host] = h }
|
|
137
|
+
opts.on('--port PORT', Integer) { |p| options[:port] = p }
|
|
138
|
+
opts.on('-h', '--help') { puts opts; exit 0 }
|
|
139
|
+
end.parse!(argv)
|
|
140
|
+
|
|
141
|
+
service = Server::Service.build(config_path: config_path)
|
|
142
|
+
Server::HttpApp.service = service
|
|
143
|
+
warn "[smart_brain] HTTP on http://#{options[:host]}:#{options[:port]} (backend: #{service.backend})"
|
|
144
|
+
Server::HttpApp.run!(host: options[:host], port: options[:port], server: :puma) do |server|
|
|
145
|
+
# Puma-specific tuning can go here.
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def mcp(config_path)
|
|
150
|
+
service = Server::Service.build(config_path: config_path)
|
|
151
|
+
Server::McpServer.new(service: service).run
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def read_payload(raw)
|
|
155
|
+
return {} if raw.nil? && $stdin.tty?
|
|
156
|
+
raw ||= $stdin.read
|
|
157
|
+
raw.to_s.strip.empty? ? {} : JSON.parse(raw, symbolize_names: true)
|
|
158
|
+
rescue JSON::ParserError => e
|
|
159
|
+
abort "invalid JSON payload: #{e.message}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def print_json(payload)
|
|
163
|
+
puts JSON.generate(payload)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
SmartBrain::CLI.run(ARGV)
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'scope_filter'
|
|
4
|
+
|
|
3
5
|
module SmartBrain
|
|
4
6
|
module Adapters
|
|
5
7
|
module SmartRag
|
|
6
8
|
class DirectClient
|
|
7
|
-
|
|
9
|
+
include ScopeFilter
|
|
10
|
+
|
|
11
|
+
def initialize(rag:, scope_mapper: nil, fail_closed: true)
|
|
8
12
|
@rag = rag
|
|
13
|
+
@scope_mapper = scope_mapper
|
|
14
|
+
@fail_closed = fail_closed
|
|
9
15
|
end
|
|
10
16
|
|
|
11
17
|
def retrieve(plan)
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
scoped_plan, required, ignored, warnings = prepare_scoped_plan(plan)
|
|
19
|
+
return scope_failure_pack(plan, ignored, warnings, prefix: 'scope-blocked') unless scoped_plan
|
|
20
|
+
|
|
21
|
+
response = rag.retrieve(plan: scoped_plan)
|
|
22
|
+
pack = normalize_pack(response, request_id: plan[:request_id])
|
|
23
|
+
enforce_scope_confirmation(pack, required: required, ignored: ignored, warnings: warnings)
|
|
14
24
|
rescue StandardError => e
|
|
15
25
|
{
|
|
16
26
|
version: '0.1',
|
|
@@ -26,7 +36,7 @@ module SmartBrain
|
|
|
26
36
|
|
|
27
37
|
private
|
|
28
38
|
|
|
29
|
-
attr_reader :rag
|
|
39
|
+
attr_reader :rag, :scope_mapper, :fail_closed
|
|
30
40
|
|
|
31
41
|
def normalize_pack(response, request_id:)
|
|
32
42
|
pack = response.is_a?(Hash) ? response : {}
|
|
@@ -38,7 +48,8 @@ module SmartBrain
|
|
|
38
48
|
evidences: Array(pack[:evidences]),
|
|
39
49
|
stats: pack[:stats] || { candidates: Array(pack[:evidences]).size, returned: Array(pack[:evidences]).size, took_ms: 0 },
|
|
40
50
|
explain: pack[:explain] || { ignored_fields: [] },
|
|
41
|
-
warnings: Array(pack[:warnings])
|
|
51
|
+
warnings: Array(pack[:warnings]),
|
|
52
|
+
scope_filter_applied: pack[:scope_filter_applied] == true
|
|
42
53
|
}
|
|
43
54
|
end
|
|
44
55
|
end
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'scope_filter'
|
|
4
|
+
|
|
3
5
|
module SmartBrain
|
|
4
6
|
module Adapters
|
|
5
7
|
module SmartRag
|
|
6
8
|
class HttpClient
|
|
7
|
-
|
|
9
|
+
include ScopeFilter
|
|
10
|
+
|
|
11
|
+
def initialize(transport:, timeout_seconds: 2, scope_mapper: nil, fail_closed: true)
|
|
8
12
|
@transport = transport
|
|
9
13
|
@timeout_seconds = timeout_seconds
|
|
14
|
+
@scope_mapper = scope_mapper
|
|
15
|
+
@fail_closed = fail_closed
|
|
10
16
|
end
|
|
11
17
|
|
|
12
18
|
def retrieve(plan)
|
|
13
19
|
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
scoped_plan, required, ignored, warnings = prepare_scoped_plan(plan)
|
|
21
|
+
return scope_failure_pack(plan, ignored, warnings, prefix: 'scope-blocked') unless scoped_plan
|
|
22
|
+
|
|
23
|
+
raw = transport.call(scoped_plan, timeout_seconds: timeout_seconds)
|
|
24
|
+
pack = build_pack(raw: raw, request_id: plan[:request_id], took_ms: elapsed_ms(started_at))
|
|
25
|
+
enforce_scope_confirmation(pack, required: required, ignored: ignored, warnings: warnings)
|
|
16
26
|
rescue Timeout::Error
|
|
17
27
|
{
|
|
18
28
|
version: '0.1',
|
|
@@ -28,7 +38,7 @@ module SmartBrain
|
|
|
28
38
|
|
|
29
39
|
private
|
|
30
40
|
|
|
31
|
-
attr_reader :transport, :timeout_seconds
|
|
41
|
+
attr_reader :transport, :timeout_seconds, :scope_mapper, :fail_closed
|
|
32
42
|
|
|
33
43
|
def build_pack(raw:, request_id:, took_ms:)
|
|
34
44
|
ignored = []
|
|
@@ -48,7 +58,8 @@ module SmartBrain
|
|
|
48
58
|
explain: {
|
|
49
59
|
ignored_fields: ignored + Array(raw.dig(:explain, :ignored_fields))
|
|
50
60
|
},
|
|
51
|
-
warnings: Array(raw[:warnings])
|
|
61
|
+
warnings: Array(raw[:warnings]),
|
|
62
|
+
scope_filter_applied: raw[:scope_filter_applied] == true
|
|
52
63
|
}
|
|
53
64
|
end
|
|
54
65
|
|
|
@@ -5,6 +5,9 @@ module SmartBrain
|
|
|
5
5
|
module SmartRag
|
|
6
6
|
class NullClient
|
|
7
7
|
def retrieve(plan)
|
|
8
|
+
business_scoped = Array(plan.dig(:scope_context, :read)).any? do |ref|
|
|
9
|
+
(ref[:type] || ref['type']).to_s != 'session'
|
|
10
|
+
end
|
|
8
11
|
{
|
|
9
12
|
version: '0.1',
|
|
10
13
|
plan_id: "local-#{plan[:request_id]}",
|
|
@@ -12,8 +15,10 @@ module SmartBrain
|
|
|
12
15
|
generated_at: Time.now.utc.iso8601,
|
|
13
16
|
evidences: [],
|
|
14
17
|
stats: { candidates: 0, returned: 0, took_ms: 0 },
|
|
15
|
-
explain: { ignored_fields: [] },
|
|
16
|
-
warnings: ['smart_rag client not configured;
|
|
18
|
+
explain: { ignored_fields: business_scoped ? ['scope_context.read'] : [] },
|
|
19
|
+
warnings: [business_scoped ? 'smart_rag scope filter unavailable: client not configured; failed closed' :
|
|
20
|
+
'smart_rag client not configured; returned empty evidences'],
|
|
21
|
+
scope_filter_applied: !business_scoped
|
|
17
22
|
}
|
|
18
23
|
end
|
|
19
24
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SmartBrain
|
|
4
|
+
module Adapters
|
|
5
|
+
module SmartRag
|
|
6
|
+
module ScopeFilter
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def prepare_scoped_plan(plan)
|
|
10
|
+
refs = Array(plan.dig(:scope_context, :read)).reject { |ref| (ref[:type] || ref['type']).to_s == 'session' }
|
|
11
|
+
return [plan, false, [], []] if refs.empty?
|
|
12
|
+
|
|
13
|
+
unless scope_mapper
|
|
14
|
+
warning = 'smart_rag scope filter unavailable: no scope mapper configured'
|
|
15
|
+
return [nil, true, ['scope_context.read'], [warning]] if fail_closed
|
|
16
|
+
return [plan, true, ['scope_context.read'], [warning]]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
mapped = scope_mapper.call(
|
|
20
|
+
domain_id: plan.dig(:debug, :caller, :domain_id),
|
|
21
|
+
scopes: refs
|
|
22
|
+
)
|
|
23
|
+
applied = mapped.fetch(:applied, true)
|
|
24
|
+
filters = mapped[:filters] || mapped.reject { |key, _value| %i[applied warnings ignored_fields].include?(key) }
|
|
25
|
+
warnings = Array(mapped[:warnings])
|
|
26
|
+
ignored = Array(mapped[:ignored_fields])
|
|
27
|
+
return [nil, true, ignored + ['scope_context.read'], warnings + ['smart_rag scope mapper did not apply filters']] if !applied && fail_closed
|
|
28
|
+
|
|
29
|
+
[plan.merge(scope_filters: filters), true, ignored, warnings]
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
warning = "smart_rag scope mapping failed: #{e.message}"
|
|
32
|
+
fail_closed ? [nil, true, ['scope_context.read'], [warning]] : [plan, true, ['scope_context.read'], [warning]]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def scope_failure_pack(plan, ignored, warnings, prefix:)
|
|
36
|
+
{
|
|
37
|
+
version: '0.2', request_id: plan[:request_id], plan_id: "#{prefix}-#{plan[:request_id]}",
|
|
38
|
+
generated_at: Time.now.utc.iso8601, evidences: [],
|
|
39
|
+
stats: { candidates: 0, returned: 0, took_ms: 0 },
|
|
40
|
+
explain: { ignored_fields: ignored }, warnings: warnings,
|
|
41
|
+
scope_filter_applied: false
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def enforce_scope_confirmation(pack, required:, ignored:, warnings:)
|
|
46
|
+
confirmed = pack[:scope_filter_applied] == true
|
|
47
|
+
pack[:explain] ||= { ignored_fields: [] }
|
|
48
|
+
pack[:explain][:ignored_fields] = Array(pack.dig(:explain, :ignored_fields)) + ignored
|
|
49
|
+
pack[:warnings] = Array(pack[:warnings]) + warnings
|
|
50
|
+
return pack unless required && !confirmed
|
|
51
|
+
|
|
52
|
+
pack[:explain][:ignored_fields] << 'scope_context.read'
|
|
53
|
+
pack[:warnings] << 'smart_rag did not confirm scope filter application'
|
|
54
|
+
pack[:evidences] = [] if fail_closed
|
|
55
|
+
pack
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|