smart_brain 0.2.0 → 0.3.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 +10 -0
- data/README.en.md +33 -0
- data/README.md +127 -1
- data/docs/installation.md +198 -0
- data/docs/media_memory_schema.md +271 -0
- data/docs/retrieval_plan.md +3 -1
- data/docs/user_guide.md +1 -0
- data/examples/07_media_memory.rb +53 -0
- data/lib/smart_brain/adapters/smart_rag/direct_client.rb +41 -0
- data/lib/smart_brain/adapters/smart_rag/http_client.rb +104 -2
- data/lib/smart_brain/adapters/smart_rag/http_transport.rb +138 -0
- data/lib/smart_brain/adapters/smart_rag/media_metadata_extractor.rb +255 -0
- data/lib/smart_brain/adapters/smart_rag/null_client.rb +37 -0
- data/lib/smart_brain/configuration.rb +4 -1
- data/lib/smart_brain/runtime.rb +19 -1
- data/lib/smart_brain/version.rb +1 -1
- data/lib/smart_brain.rb +15 -2
- metadata +36 -42
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'open3'
|
|
5
|
+
|
|
6
|
+
module SmartBrain
|
|
7
|
+
module Adapters
|
|
8
|
+
module SmartRag
|
|
9
|
+
# Extracts technical metadata from media files (image / audio / video).
|
|
10
|
+
#
|
|
11
|
+
# Stage 1 of the media memory plan (docs/media_memory_schema.md):
|
|
12
|
+
# - image -> Python Pillow bridge (same pattern as MarkitdownBridge)
|
|
13
|
+
# - audio -> ffprobe
|
|
14
|
+
# - video -> ffprobe
|
|
15
|
+
#
|
|
16
|
+
# Degradation policy (D5): when a dependency is missing the extractor
|
|
17
|
+
# returns the default media object with a warning instead of raising,
|
|
18
|
+
# so ingestion can still store file-level metadata.
|
|
19
|
+
#
|
|
20
|
+
# NOTE: This component lives in smart_brain during stage 1 (testable
|
|
21
|
+
# without a SmartRAG database). Stage 3 will sync it into the SmartRAG
|
|
22
|
+
# project as SmartRAG::Core::MediaMetadataExtractor and wire it into
|
|
23
|
+
# DocumentProcessor#extract_metadata.
|
|
24
|
+
class MediaMetadataExtractor
|
|
25
|
+
IMAGE_EXTS = %w[.jpg .jpeg .png .gif .webp .bmp .tif .tiff .heic .heif].freeze
|
|
26
|
+
AUDIO_EXTS = %w[.mp3 .wav .m4a .aac .flac .ogg .oga .opus .wma .aif .aiff].freeze
|
|
27
|
+
VIDEO_EXTS = %w[.mp4 .mkv .mov .webm .avi .flv .wmv .m4v .ts .mpeg .mpg].freeze
|
|
28
|
+
|
|
29
|
+
# Wide schema shared by all three media types; type-specific fields
|
|
30
|
+
# stay nil (design decision D1).
|
|
31
|
+
DEFAULT_MEDIA = {
|
|
32
|
+
format: nil,
|
|
33
|
+
width: nil,
|
|
34
|
+
height: nil,
|
|
35
|
+
dpi: nil,
|
|
36
|
+
color_mode: nil,
|
|
37
|
+
orientation: nil,
|
|
38
|
+
exif: {},
|
|
39
|
+
duration_ms: nil,
|
|
40
|
+
bitrate_bps: nil,
|
|
41
|
+
sample_rate_hz: nil,
|
|
42
|
+
channels: nil,
|
|
43
|
+
codec: nil,
|
|
44
|
+
fps: nil,
|
|
45
|
+
audio_codec: nil
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
# Extract technical metadata from a media file.
|
|
49
|
+
# @param [String] file_path path to the media file
|
|
50
|
+
# @param [String, Symbol, nil] media_type explicit type override
|
|
51
|
+
# @return [Hash] { media_type:, media: {...}, warnings: [...] }
|
|
52
|
+
def extract(file_path, media_type: nil)
|
|
53
|
+
detected = media_type || detect_media_type(file_path)
|
|
54
|
+
return { media_type: 'other', media: DEFAULT_MEDIA.dup, warnings: [] } unless detected
|
|
55
|
+
|
|
56
|
+
type = detected.to_sym
|
|
57
|
+
media = DEFAULT_MEDIA.dup
|
|
58
|
+
warnings = []
|
|
59
|
+
|
|
60
|
+
case type
|
|
61
|
+
when :image then extract_image(file_path, media, warnings)
|
|
62
|
+
when :audio then extract_audio(file_path, media, warnings)
|
|
63
|
+
when :video then extract_video(file_path, media, warnings)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
{ media_type: type.to_s, media: media, warnings: warnings }
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
{
|
|
69
|
+
media_type: (type ? type.to_s : 'other'),
|
|
70
|
+
media: (defined?(media) && media) || DEFAULT_MEDIA.dup,
|
|
71
|
+
warnings: ["media metadata extraction failed: #{e.message}"]
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Detect media type from file extension.
|
|
76
|
+
# @param [String] file_path
|
|
77
|
+
# @return [Symbol, nil] :image | :audio | :video | nil (unknown)
|
|
78
|
+
def detect_media_type(file_path)
|
|
79
|
+
ext = File.extname(file_path.to_s).downcase
|
|
80
|
+
return :image if IMAGE_EXTS.include?(ext)
|
|
81
|
+
return :audio if AUDIO_EXTS.include?(ext)
|
|
82
|
+
return :video if VIDEO_EXTS.include?(ext)
|
|
83
|
+
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Check whether the image dependency (Pillow) is available.
|
|
88
|
+
def pillow_available?
|
|
89
|
+
return @pillow_available unless @pillow_available.nil?
|
|
90
|
+
|
|
91
|
+
@pillow_available = system('python3', '-c', 'import PIL', out: File::NULL, err: File::NULL) ||
|
|
92
|
+
system('python', '-c', 'import PIL', out: File::NULL, err: File::NULL)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check whether ffprobe is available.
|
|
96
|
+
def ffprobe_available?
|
|
97
|
+
return @ffprobe_available unless @ffprobe_available.nil?
|
|
98
|
+
|
|
99
|
+
@ffprobe_available = !ffprobe_cmd.nil?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def extract_image(file_path, media, warnings)
|
|
105
|
+
unless pillow_available?
|
|
106
|
+
warnings << 'Pillow (PIL) not available; image metadata skipped'
|
|
107
|
+
return media
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
python = %w[python3 python].find { |cmd| system(cmd, '-c', 'import PIL', out: File::NULL, err: File::NULL) }
|
|
111
|
+
script = <<~PYTHON
|
|
112
|
+
import json, sys
|
|
113
|
+
from PIL import Image
|
|
114
|
+
|
|
115
|
+
path = sys.argv[1]
|
|
116
|
+
img = Image.open(path)
|
|
117
|
+
info = {
|
|
118
|
+
'format': (img.format or '').lower() or None,
|
|
119
|
+
'width': img.width,
|
|
120
|
+
'height': img.height,
|
|
121
|
+
'color_mode': img.mode,
|
|
122
|
+
}
|
|
123
|
+
dpi = img.info.get('dpi')
|
|
124
|
+
info['dpi'] = round(dpi[0]) if dpi and isinstance(dpi[0], (int, float))
|
|
125
|
+
exif = {}
|
|
126
|
+
try:
|
|
127
|
+
from PIL import ExifTags
|
|
128
|
+
raw = img._getexif() if hasattr(img, '_getexif') else None
|
|
129
|
+
if raw:
|
|
130
|
+
for k, v in raw.items():
|
|
131
|
+
name = ExifTags.TAGS.get(k, str(k))
|
|
132
|
+
if v is None or isinstance(v, (int, float, str)):
|
|
133
|
+
exif[name] = v
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
info['exif'] = exif
|
|
137
|
+
print(json.dumps(info))
|
|
138
|
+
PYTHON
|
|
139
|
+
|
|
140
|
+
output, status = Open3.capture2e(python, '-c', script, file_path)
|
|
141
|
+
unless status.success?
|
|
142
|
+
warnings << "image metadata extraction failed: #{output.strip}"
|
|
143
|
+
return media
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
parsed = JSON.parse(output)
|
|
147
|
+
media[:format] = parsed['format']
|
|
148
|
+
media[:width] = parsed['width']
|
|
149
|
+
media[:height] = parsed['height']
|
|
150
|
+
media[:color_mode] = parsed['color_mode']
|
|
151
|
+
media[:dpi] = parsed['dpi']
|
|
152
|
+
media[:exif] = parsed['exif'] || {}
|
|
153
|
+
media[:orientation] = derive_orientation(media[:width], media[:height])
|
|
154
|
+
media
|
|
155
|
+
rescue JSON::ParserError, SystemCallError => e
|
|
156
|
+
warnings << "image metadata extraction failed: #{e.message}"
|
|
157
|
+
media
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def extract_audio(file_path, media, warnings)
|
|
161
|
+
probe = probe_ffprobe(file_path, warnings)
|
|
162
|
+
return media unless probe
|
|
163
|
+
|
|
164
|
+
format_info = probe['format'] || {}
|
|
165
|
+
stream = probe['streams'].to_a.find { |s| s['codec_type'] == 'audio' } || probe['streams'].to_a.first
|
|
166
|
+
|
|
167
|
+
media[:format] = File.extname(file_path.to_s).downcase.sub('.', '')
|
|
168
|
+
media[:codec] = stream && stream['codec_name']
|
|
169
|
+
media[:duration_ms] = ms_of(format_info['duration'])
|
|
170
|
+
media[:bitrate_bps] = int_of(format_info['bit_rate']) || (stream && int_of(stream['bit_rate']))
|
|
171
|
+
media[:sample_rate_hz] = stream && int_of(stream['sample_rate'])
|
|
172
|
+
media[:channels] = stream && int_of(stream['channels'])
|
|
173
|
+
media
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def extract_video(file_path, media, warnings)
|
|
177
|
+
probe = probe_ffprobe(file_path, warnings)
|
|
178
|
+
return media unless probe
|
|
179
|
+
|
|
180
|
+
format_info = probe['format'] || {}
|
|
181
|
+
streams = probe['streams'].to_a
|
|
182
|
+
video_stream = streams.find { |s| s['codec_type'] == 'video' }
|
|
183
|
+
audio_stream = streams.find { |s| s['codec_type'] == 'audio' }
|
|
184
|
+
|
|
185
|
+
media[:format] = File.extname(file_path.to_s).downcase.sub('.', '')
|
|
186
|
+
media[:duration_ms] = ms_of(format_info['duration'])
|
|
187
|
+
media[:bitrate_bps] = int_of(format_info['bit_rate'])
|
|
188
|
+
if video_stream
|
|
189
|
+
media[:width] = int_of(video_stream['width'])
|
|
190
|
+
media[:height] = int_of(video_stream['height'])
|
|
191
|
+
media[:codec] = video_stream['codec_name']
|
|
192
|
+
media[:fps] = fps_of(video_stream)
|
|
193
|
+
end
|
|
194
|
+
media[:audio_codec] = audio_stream && audio_stream['codec_name']
|
|
195
|
+
media[:orientation] = derive_orientation(media[:width], media[:height])
|
|
196
|
+
media
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def probe_ffprobe(file_path, warnings)
|
|
200
|
+
cmd = ffprobe_cmd
|
|
201
|
+
unless cmd
|
|
202
|
+
warnings << 'ffprobe not available; audio/video metadata skipped'
|
|
203
|
+
return nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
args = [cmd, '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', file_path]
|
|
207
|
+
output, status, = Open3.capture3(*args)
|
|
208
|
+
unless status.success?
|
|
209
|
+
warnings << "ffprobe failed: #{output.strip}"
|
|
210
|
+
return nil
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
JSON.parse(output)
|
|
214
|
+
rescue JSON::ParserError, SystemCallError, Errno::ENOENT => e
|
|
215
|
+
warnings << "ffprobe failed: #{e.message}"
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def ffprobe_cmd
|
|
220
|
+
@ffprobe_cmd ||= %w[ffprobe].find { |c| system('which', c, out: File::NULL, err: File::NULL) }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def ms_of(value)
|
|
224
|
+
return nil if value.nil?
|
|
225
|
+
|
|
226
|
+
(value.to_f * 1000).round
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def int_of(value)
|
|
230
|
+
return nil if value.nil?
|
|
231
|
+
|
|
232
|
+
Integer(value.to_s, exception: false)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def fps_of(stream)
|
|
236
|
+
rate = stream['avg_frame_rate'].to_s
|
|
237
|
+
return nil if rate.empty?
|
|
238
|
+
|
|
239
|
+
num, den = rate.split('/')
|
|
240
|
+
return nil if num.nil?
|
|
241
|
+
|
|
242
|
+
den = '1' if den.to_s.empty? || den.to_i.zero?
|
|
243
|
+
(num.to_f / den.to_f).round(2)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def derive_orientation(width, height)
|
|
247
|
+
return nil unless width && height
|
|
248
|
+
return 'square' if width == height
|
|
249
|
+
|
|
250
|
+
width > height ? 'landscape' : 'portrait'
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
@@ -4,6 +4,8 @@ module SmartBrain
|
|
|
4
4
|
module Adapters
|
|
5
5
|
module SmartRag
|
|
6
6
|
class NullClient
|
|
7
|
+
WRITE_OPERATIONS = %i[document media image audio video].freeze
|
|
8
|
+
|
|
7
9
|
def retrieve(plan)
|
|
8
10
|
business_scoped = Array(plan.dig(:scope_context, :read)).any? do |ref|
|
|
9
11
|
(ref[:type] || ref['type']).to_s != 'session'
|
|
@@ -21,6 +23,41 @@ module SmartBrain
|
|
|
21
23
|
scope_filter_applied: !business_scoped
|
|
22
24
|
}
|
|
23
25
|
end
|
|
26
|
+
|
|
27
|
+
WRITE_OPERATIONS.each do |type|
|
|
28
|
+
define_method("add_#{type}") do |_source, _options = {}|
|
|
29
|
+
{ status: 'unsupported', media_type: type.to_s, section_count: 0,
|
|
30
|
+
warnings: ["smart_rag client not configured; add_#{type} ignored"] }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def enqueue_media(_source, _options = {})
|
|
35
|
+
{ status: 'unsupported', warnings: ['smart_rag client not configured; enqueue_media ignored'] }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def media_job(job_id)
|
|
39
|
+
{ status: 'unsupported', job_id: job_id, warnings: ['smart_rag client not configured; media_job ignored'] }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def media_jobs(status: nil, limit: 20, offset: 0)
|
|
43
|
+
{ status: 'unsupported', jobs: [], total: 0,
|
|
44
|
+
warnings: ['smart_rag client not configured; media_jobs ignored'] }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def cancel_media_job(job_id)
|
|
48
|
+
{ status: 'unsupported', job_id: job_id,
|
|
49
|
+
warnings: ['smart_rag client not configured; cancel_media_job ignored'] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def retry_media_job(job_id)
|
|
53
|
+
{ status: 'unsupported', job_id: job_id,
|
|
54
|
+
warnings: ['smart_rag client not configured; retry_media_job ignored'] }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def media_job_statistics
|
|
58
|
+
{ status: 'unsupported', counts: {}, total: 0,
|
|
59
|
+
warnings: ['smart_rag client not configured; media_job_statistics ignored'] }
|
|
60
|
+
end
|
|
24
61
|
end
|
|
25
62
|
end
|
|
26
63
|
end
|
|
@@ -85,7 +85,10 @@ module SmartBrain
|
|
|
85
85
|
def database_config
|
|
86
86
|
cfg = (storage.fetch(:database, {}) || {}).dup
|
|
87
87
|
%i[host port database user password].each do |key|
|
|
88
|
-
|
|
88
|
+
# The `database` key maps to SMARTBRAIN_DB_NAME to match the documented
|
|
89
|
+
# env contract (SMARTBRAIN_DB_{HOST,PORT,NAME,USER,PASSWORD}).
|
|
90
|
+
suffix = key == :database ? 'NAME' : key.to_s.upcase
|
|
91
|
+
env_key = "SMARTBRAIN_DB_#{suffix}"
|
|
89
92
|
cfg[key] = ENV[env_key] if ENV.key?(env_key)
|
|
90
93
|
end
|
|
91
94
|
cfg
|
data/lib/smart_brain/runtime.rb
CHANGED
|
@@ -264,7 +264,8 @@ module SmartBrain
|
|
|
264
264
|
result
|
|
265
265
|
end
|
|
266
266
|
|
|
267
|
-
def compose_context(session_id:, user_message:, agent_state: {}, domain_id: nil, scope_context: nil
|
|
267
|
+
def compose_context(session_id:, user_message:, agent_state: {}, domain_id: nil, scope_context: nil,
|
|
268
|
+
resource_filters: {})
|
|
268
269
|
started_at = monotonic_time
|
|
269
270
|
resolved = resolve_scopes(domain_id: domain_id, session_id: session_id, scope_context: scope_context)
|
|
270
271
|
recent_turns = event_store.recent_turns(session_id: session_id, limit: config.composition.fetch(:recent_turns_max, 8))
|
|
@@ -279,6 +280,7 @@ module SmartBrain
|
|
|
279
280
|
refs: refs
|
|
280
281
|
)
|
|
281
282
|
plan[:scope_context] = public_scope_context(resolved)
|
|
283
|
+
plan[:global_filters] = plan.fetch(:global_filters, {}).merge(symbolize_keys(resource_filters))
|
|
282
284
|
plan[:debug] = (plan[:debug] || {}).merge(
|
|
283
285
|
caller: { app: 'smart_brain', domain_id: resolved[:domain_id], session_id: session_id }
|
|
284
286
|
)
|
|
@@ -363,6 +365,18 @@ module SmartBrain
|
|
|
363
365
|
conflict_resolver.resolve(result)[:selected].first(limit)
|
|
364
366
|
end
|
|
365
367
|
|
|
368
|
+
def add_document(source:, options: {}) = smart_rag_client.add_document(source, options)
|
|
369
|
+
def add_media(source:, options: {}) = smart_rag_client.add_media(source, options)
|
|
370
|
+
def add_image(source:, options: {}) = smart_rag_client.add_image(source, options)
|
|
371
|
+
def add_audio(source:, options: {}) = smart_rag_client.add_audio(source, options)
|
|
372
|
+
def add_video(source:, options: {}) = smart_rag_client.add_video(source, options)
|
|
373
|
+
def enqueue_media(source:, options: {}) = smart_rag_client.enqueue_media(source, options)
|
|
374
|
+
def media_job(job_id:) = smart_rag_client.media_job(job_id)
|
|
375
|
+
def media_jobs(status: nil, limit: 20, offset: 0) = smart_rag_client.media_jobs(status: status, limit: limit, offset: offset)
|
|
376
|
+
def cancel_media_job(job_id:) = smart_rag_client.cancel_media_job(job_id)
|
|
377
|
+
def retry_media_job(job_id:) = smart_rag_client.retry_media_job(job_id)
|
|
378
|
+
def media_job_statistics = smart_rag_client.media_job_statistics
|
|
379
|
+
|
|
366
380
|
def memory_item(id:)
|
|
367
381
|
memory_store.find_item(id: id)
|
|
368
382
|
end
|
|
@@ -413,6 +427,10 @@ module SmartBrain
|
|
|
413
427
|
raise
|
|
414
428
|
end
|
|
415
429
|
|
|
430
|
+
def symbolize_keys(hash)
|
|
431
|
+
hash.each_with_object({}) { |(key, value), result| result[key.to_sym] = value }
|
|
432
|
+
end
|
|
433
|
+
|
|
416
434
|
def scope_validation_failure!(message)
|
|
417
435
|
error = ArgumentError.new(message)
|
|
418
436
|
tracker.log_scope_validation_failure(error)
|
data/lib/smart_brain/version.rb
CHANGED
data/lib/smart_brain.rb
CHANGED
|
@@ -18,13 +18,26 @@ module SmartBrain
|
|
|
18
18
|
runtime.commit_turn(session_id: session_id, turn_events: turn_events, domain_id: domain_id, scope_context: scope_context)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def compose_context(session_id:, user_message:, agent_state: {}, domain_id: nil, scope_context: nil
|
|
21
|
+
def compose_context(session_id:, user_message:, agent_state: {}, domain_id: nil, scope_context: nil,
|
|
22
|
+
resource_filters: {})
|
|
22
23
|
runtime.compose_context(
|
|
23
24
|
session_id: session_id, user_message: user_message, agent_state: agent_state,
|
|
24
|
-
domain_id: domain_id, scope_context: scope_context
|
|
25
|
+
domain_id: domain_id, scope_context: scope_context, resource_filters: resource_filters
|
|
25
26
|
)
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
def add_document(source:, options: {}) = runtime.add_document(source: source, options: options)
|
|
30
|
+
def add_media(source:, options: {}) = runtime.add_media(source: source, options: options)
|
|
31
|
+
def add_image(source:, options: {}) = runtime.add_image(source: source, options: options)
|
|
32
|
+
def add_audio(source:, options: {}) = runtime.add_audio(source: source, options: options)
|
|
33
|
+
def add_video(source:, options: {}) = runtime.add_video(source: source, options: options)
|
|
34
|
+
def enqueue_media(source:, options: {}) = runtime.enqueue_media(source: source, options: options)
|
|
35
|
+
def media_job(job_id:) = runtime.media_job(job_id: job_id)
|
|
36
|
+
def media_jobs(status: nil, limit: 20, offset: 0) = runtime.media_jobs(status: status, limit: limit, offset: offset)
|
|
37
|
+
def cancel_media_job(job_id:) = runtime.cancel_media_job(job_id: job_id)
|
|
38
|
+
def retry_media_job(job_id:) = runtime.retry_media_job(job_id: job_id)
|
|
39
|
+
def media_job_statistics = runtime.media_job_statistics
|
|
40
|
+
|
|
28
41
|
def search_memory(session_id:, query:, limit: nil, domain_id: nil, scope_context: nil)
|
|
29
42
|
runtime.search_memory(
|
|
30
43
|
session_id: session_id, query: query, limit: limit, domain_id: domain_id, scope_context: scope_context
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smart_brain
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- SmartBrain Team
|
|
@@ -9,34 +9,6 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: dry-struct
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - "~>"
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '1.7'
|
|
19
|
-
type: :runtime
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - "~>"
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '1.7'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: dry-validation
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '1.11'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '1.11'
|
|
40
12
|
- !ruby/object:Gem::Dependency
|
|
41
13
|
name: faraday
|
|
42
14
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -52,19 +24,19 @@ dependencies:
|
|
|
52
24
|
- !ruby/object:Gem::Version
|
|
53
25
|
version: '2.11'
|
|
54
26
|
- !ruby/object:Gem::Dependency
|
|
55
|
-
name:
|
|
27
|
+
name: faraday-multipart
|
|
56
28
|
requirement: !ruby/object:Gem::Requirement
|
|
57
29
|
requirements:
|
|
58
30
|
- - "~>"
|
|
59
31
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '
|
|
32
|
+
version: '1.0'
|
|
61
33
|
type: :runtime
|
|
62
34
|
prerelease: false
|
|
63
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
36
|
requirements:
|
|
65
37
|
- - "~>"
|
|
66
38
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '
|
|
39
|
+
version: '1.0'
|
|
68
40
|
- !ruby/object:Gem::Dependency
|
|
69
41
|
name: pg
|
|
70
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -94,47 +66,47 @@ dependencies:
|
|
|
94
66
|
- !ruby/object:Gem::Version
|
|
95
67
|
version: '7.2'
|
|
96
68
|
- !ruby/object:Gem::Dependency
|
|
97
|
-
name:
|
|
69
|
+
name: rackup
|
|
98
70
|
requirement: !ruby/object:Gem::Requirement
|
|
99
71
|
requirements:
|
|
100
72
|
- - "~>"
|
|
101
73
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: '
|
|
74
|
+
version: '2.1'
|
|
103
75
|
type: :runtime
|
|
104
76
|
prerelease: false
|
|
105
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
106
78
|
requirements:
|
|
107
79
|
- - "~>"
|
|
108
80
|
- !ruby/object:Gem::Version
|
|
109
|
-
version: '
|
|
81
|
+
version: '2.1'
|
|
110
82
|
- !ruby/object:Gem::Dependency
|
|
111
|
-
name:
|
|
83
|
+
name: sequel
|
|
112
84
|
requirement: !ruby/object:Gem::Requirement
|
|
113
85
|
requirements:
|
|
114
86
|
- - "~>"
|
|
115
87
|
- !ruby/object:Gem::Version
|
|
116
|
-
version: '
|
|
88
|
+
version: '5.87'
|
|
117
89
|
type: :runtime
|
|
118
90
|
prerelease: false
|
|
119
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
120
92
|
requirements:
|
|
121
93
|
- - "~>"
|
|
122
94
|
- !ruby/object:Gem::Version
|
|
123
|
-
version: '
|
|
95
|
+
version: '5.87'
|
|
124
96
|
- !ruby/object:Gem::Dependency
|
|
125
|
-
name:
|
|
97
|
+
name: sinatra
|
|
126
98
|
requirement: !ruby/object:Gem::Requirement
|
|
127
99
|
requirements:
|
|
128
100
|
- - "~>"
|
|
129
101
|
- !ruby/object:Gem::Version
|
|
130
|
-
version: '
|
|
102
|
+
version: '4.2'
|
|
131
103
|
type: :runtime
|
|
132
104
|
prerelease: false
|
|
133
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
134
106
|
requirements:
|
|
135
107
|
- - "~>"
|
|
136
108
|
- !ruby/object:Gem::Version
|
|
137
|
-
version: '
|
|
109
|
+
version: '4.2'
|
|
138
110
|
- !ruby/object:Gem::Dependency
|
|
139
111
|
name: bundler
|
|
140
112
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -259,7 +231,9 @@ files:
|
|
|
259
231
|
- docs/context_package.md
|
|
260
232
|
- docs/evidence_pack.md
|
|
261
233
|
- docs/gap_vs_mempal.md
|
|
234
|
+
- docs/installation.md
|
|
262
235
|
- docs/mcp.md
|
|
236
|
+
- docs/media_memory_schema.md
|
|
263
237
|
- docs/memory_types.md
|
|
264
238
|
- docs/multi_scope_memory_refactor_plan.md
|
|
265
239
|
- docs/multi_scope_migration.md
|
|
@@ -274,11 +248,14 @@ files:
|
|
|
274
248
|
- examples/04_ollama_llm.rb
|
|
275
249
|
- examples/05_smart_rag_integration.rb
|
|
276
250
|
- examples/06_multi_scope_memory.rb
|
|
251
|
+
- examples/07_media_memory.rb
|
|
277
252
|
- examples/README.md
|
|
278
253
|
- exe/smart_brain
|
|
279
254
|
- lib/smart_brain.rb
|
|
280
255
|
- lib/smart_brain/adapters/smart_rag/direct_client.rb
|
|
281
256
|
- lib/smart_brain/adapters/smart_rag/http_client.rb
|
|
257
|
+
- lib/smart_brain/adapters/smart_rag/http_transport.rb
|
|
258
|
+
- lib/smart_brain/adapters/smart_rag/media_metadata_extractor.rb
|
|
282
259
|
- lib/smart_brain/adapters/smart_rag/null_client.rb
|
|
283
260
|
- lib/smart_brain/adapters/smart_rag/scope_filter.rb
|
|
284
261
|
- lib/smart_brain/configuration.rb
|
|
@@ -329,6 +306,23 @@ licenses:
|
|
|
329
306
|
metadata:
|
|
330
307
|
source_code_uri: https://github.com/zhuangbiaowei/smart_brain
|
|
331
308
|
changelog_uri: https://github.com/zhuangbiaowei/smart_brain/blob/master/CHANGELOG.md
|
|
309
|
+
documentation_uri: https://github.com/zhuangbiaowei/smart_brain/blob/master/docs/user_guide.md
|
|
310
|
+
post_install_message: |
|
|
311
|
+
SmartBrain 已安装(0.3.0)。默认零外部依赖:memory 后端 + stub LLM。
|
|
312
|
+
|
|
313
|
+
快速开始:
|
|
314
|
+
smart_brain --version
|
|
315
|
+
smart_brain status
|
|
316
|
+
ruby -e "require 'smart_brain'; SmartBrain.configure; p SmartBrain.compose_context(session_id: 's', user_message: '你好')"
|
|
317
|
+
|
|
318
|
+
常用配置(环境变量):
|
|
319
|
+
SMARTBRAIN_BACKEND=postgres # 持久化(需先建库,或 smart_brain migrate)
|
|
320
|
+
SMARTBRAIN_DB_NAME=smart_brain_development # 其余 SMARTBRAIN_DB_{HOST,PORT,USER,PASSWORD}
|
|
321
|
+
SMARTBRAIN_LLM_PROVIDER=ollama # 真 LLM 摘要/重排(需本地 ollama + 生成式模型如 qwen3/llama2)
|
|
322
|
+
SMARTBRAIN_LLM_BASE_URL / SMARTBRAIN_LLM_API_KEY / SMARTBRAIN_LLM_MODEL
|
|
323
|
+
|
|
324
|
+
资源 RAG(可选):gem install smart_rag,再用 SmartBrain::Adapters::SmartRag::DirectClient / HttpClient。
|
|
325
|
+
详见 README 与 docs/user_guide.md、docs/installation.md。
|
|
332
326
|
rdoc_options: []
|
|
333
327
|
require_paths:
|
|
334
328
|
- lib
|
|
@@ -343,7 +337,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
343
337
|
- !ruby/object:Gem::Version
|
|
344
338
|
version: '0'
|
|
345
339
|
requirements: []
|
|
346
|
-
rubygems_version: 4.0.
|
|
340
|
+
rubygems_version: 4.0.16
|
|
347
341
|
specification_version: 4
|
|
348
342
|
summary: Agent memory runtime and context composer
|
|
349
343
|
test_files: []
|