swarm_memory 2.2.2 → 2.2.4
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/lib/swarm_memory/core/semantic_index.rb +10 -2
- data/lib/swarm_memory/core/storage.rb +7 -2
- data/lib/swarm_memory/dsl/memory_config.rb +37 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +23 -6
- data/lib/swarm_memory/tools/memory_glob.rb +1 -1
- data/lib/swarm_memory/tools/memory_grep.rb +3 -1
- data/lib/swarm_memory/tools/memory_read.rb +35 -31
- data/lib/swarm_memory/tools/title_lookup.rb +65 -0
- data/lib/swarm_memory/version.rb +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 768a8a8cd1d0ab5a3edff5e6e0c54e3fdce1e6c6af281d6a39285019167b87c8
|
|
4
|
+
data.tar.gz: aa9e396d3c72e53bf49459828c635148205c523e6d16bb42e7f6e4e5856a1d4c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c35aad639f910180b0bcd73db10b8db18f4cb9bde3d136789e77b26ac1a1a2a66fc476b1cce9731bf06566ad8bf9bfbc8edfe0c513b62a303c79346c6efb5b52
|
|
7
|
+
data.tar.gz: 18f0c1c5fc97d2946174e43d61c86493cbaa31626a8c6ecbf019ad12ca9654ebdeb77475305a79fb1c11ed85eb85b35be44fad349bb56806e039ea6afafea247
|
|
@@ -181,6 +181,9 @@ module SwarmMemory
|
|
|
181
181
|
|
|
182
182
|
# Calculate hybrid scores combining semantic similarity and keyword matching
|
|
183
183
|
#
|
|
184
|
+
# When keyword_score is 0 (no tag matches), falls back to pure semantic scoring
|
|
185
|
+
# to avoid penalizing results that have excellent semantic matches but no tag overlap.
|
|
186
|
+
#
|
|
184
187
|
# @param results [Array<Hash>] Results with semantic :similarity scores
|
|
185
188
|
# @param query_keywords [Array<String>] Keywords from query
|
|
186
189
|
# @return [Array<Hash>] Results with updated :similarity (hybrid score) and debug info
|
|
@@ -189,8 +192,13 @@ module SwarmMemory
|
|
|
189
192
|
semantic_score = result[:similarity]
|
|
190
193
|
keyword_score = calculate_keyword_score(result, query_keywords)
|
|
191
194
|
|
|
192
|
-
#
|
|
193
|
-
|
|
195
|
+
# Fallback to pure semantic when no keyword matches
|
|
196
|
+
# This prevents penalizing results with excellent semantic matches but no tag overlap
|
|
197
|
+
hybrid_score = if keyword_score.zero?
|
|
198
|
+
semantic_score
|
|
199
|
+
else
|
|
200
|
+
(@semantic_weight * semantic_score) + (@keyword_weight * keyword_score)
|
|
201
|
+
end
|
|
194
202
|
|
|
195
203
|
# Update result with hybrid score and debug info
|
|
196
204
|
result.merge(
|
|
@@ -18,7 +18,9 @@ module SwarmMemory
|
|
|
18
18
|
#
|
|
19
19
|
# @param adapter [Adapters::Base] Storage adapter
|
|
20
20
|
# @param embedder [Embeddings::Embedder, nil] Optional embedder for semantic search
|
|
21
|
-
|
|
21
|
+
# @param semantic_weight [Float, nil] Weight for semantic similarity in hybrid search (0.0-1.0)
|
|
22
|
+
# @param keyword_weight [Float, nil] Weight for keyword matching in hybrid search (0.0-1.0)
|
|
23
|
+
def initialize(adapter:, embedder: nil, semantic_weight: nil, keyword_weight: nil)
|
|
22
24
|
raise ArgumentError, "adapter is required" unless adapter.is_a?(Adapters::Base)
|
|
23
25
|
|
|
24
26
|
@adapter = adapter
|
|
@@ -26,7 +28,10 @@ module SwarmMemory
|
|
|
26
28
|
|
|
27
29
|
# Create semantic index if embedder is provided
|
|
28
30
|
@semantic_index = if embedder
|
|
29
|
-
|
|
31
|
+
index_options = { adapter: adapter, embedder: embedder }
|
|
32
|
+
index_options[:semantic_weight] = semantic_weight if semantic_weight
|
|
33
|
+
index_options[:keyword_weight] = keyword_weight if keyword_weight
|
|
34
|
+
SemanticIndex.new(**index_options)
|
|
30
35
|
end
|
|
31
36
|
end
|
|
32
37
|
|
|
@@ -84,6 +84,43 @@ module SwarmMemory
|
|
|
84
84
|
@mode = value.to_sym
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# DSL method to set/get semantic weight for hybrid search
|
|
88
|
+
#
|
|
89
|
+
# Controls how much semantic (embedding) similarity affects search results.
|
|
90
|
+
# Default is 0.5 (50%). Set to 1.0 for pure semantic search.
|
|
91
|
+
#
|
|
92
|
+
# @param value [Float, nil] Weight between 0.0 and 1.0
|
|
93
|
+
# @return [Float, nil] Current semantic weight
|
|
94
|
+
#
|
|
95
|
+
# @example Pure semantic search (no keyword penalty)
|
|
96
|
+
# semantic_weight 1.0
|
|
97
|
+
# keyword_weight 0.0
|
|
98
|
+
def semantic_weight(value = nil)
|
|
99
|
+
if value.nil?
|
|
100
|
+
@adapter_options[:semantic_weight]
|
|
101
|
+
else
|
|
102
|
+
@adapter_options[:semantic_weight] = value.to_f
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# DSL method to set/get keyword weight for hybrid search
|
|
107
|
+
#
|
|
108
|
+
# Controls how much keyword (tag) matching affects search results.
|
|
109
|
+
# Default is 0.5 (50%). Set to 0.0 to disable keyword matching.
|
|
110
|
+
#
|
|
111
|
+
# @param value [Float, nil] Weight between 0.0 and 1.0
|
|
112
|
+
# @return [Float, nil] Current keyword weight
|
|
113
|
+
#
|
|
114
|
+
# @example Disable keyword matching
|
|
115
|
+
# keyword_weight 0.0
|
|
116
|
+
def keyword_weight(value = nil)
|
|
117
|
+
if value.nil?
|
|
118
|
+
@adapter_options[:keyword_weight]
|
|
119
|
+
else
|
|
120
|
+
@adapter_options[:keyword_weight] = value.to_f
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
87
124
|
# Check if memory is enabled
|
|
88
125
|
#
|
|
89
126
|
# @return [Boolean] True if adapter is configured with required options
|
|
@@ -110,10 +110,12 @@ module SwarmMemory
|
|
|
110
110
|
# MemoryConfig object (from DSL)
|
|
111
111
|
[config.adapter_type, config.adapter_options]
|
|
112
112
|
elsif config.is_a?(Hash)
|
|
113
|
-
# Hash (from YAML)
|
|
113
|
+
# Hash (from YAML) - symbolize keys for adapter compatibility
|
|
114
114
|
adapter = (config[:adapter] || config["adapter"] || :filesystem).to_sym
|
|
115
|
-
options = config.reject { |k, _v|
|
|
116
|
-
|
|
115
|
+
options = config.reject { |k, _v| [:adapter, "adapter", :mode, "mode"].include?(k) }
|
|
116
|
+
# Symbolize keys so adapter receives keyword arguments correctly
|
|
117
|
+
symbolized_options = options.transform_keys { |k| k.to_s.to_sym }
|
|
118
|
+
[adapter, symbolized_options]
|
|
117
119
|
else
|
|
118
120
|
raise SwarmSDK::ConfigurationError, "Invalid memory configuration for #{agent_name}"
|
|
119
121
|
end
|
|
@@ -125,7 +127,17 @@ module SwarmMemory
|
|
|
125
127
|
raise SwarmSDK::ConfigurationError, "#{e.message} for agent #{agent_name}"
|
|
126
128
|
end
|
|
127
129
|
|
|
128
|
-
#
|
|
130
|
+
# Extract hybrid search weights and other SDK-level config (before passing to adapter)
|
|
131
|
+
# Keys are already symbolized at this point
|
|
132
|
+
semantic_weight = adapter_options.delete(:semantic_weight)
|
|
133
|
+
keyword_weight = adapter_options.delete(:keyword_weight)
|
|
134
|
+
|
|
135
|
+
# Remove other SDK-level threshold configs that shouldn't go to adapter
|
|
136
|
+
adapter_options.delete(:discovery_threshold)
|
|
137
|
+
adapter_options.delete(:discovery_threshold_short)
|
|
138
|
+
adapter_options.delete(:adaptive_word_cutoff)
|
|
139
|
+
|
|
140
|
+
# Instantiate adapter with options (weights removed, adapter doesn't need them)
|
|
129
141
|
# Note: Adapter is responsible for validating its own requirements
|
|
130
142
|
begin
|
|
131
143
|
adapter = adapter_class.new(**adapter_options)
|
|
@@ -137,8 +149,13 @@ module SwarmMemory
|
|
|
137
149
|
# Create embedder for semantic search
|
|
138
150
|
embedder = Embeddings::InformersEmbedder.new
|
|
139
151
|
|
|
140
|
-
# Create storage with embedder
|
|
141
|
-
Core::Storage.new(
|
|
152
|
+
# Create storage with embedder and hybrid search weights
|
|
153
|
+
Core::Storage.new(
|
|
154
|
+
adapter: adapter,
|
|
155
|
+
embedder: embedder,
|
|
156
|
+
semantic_weight: semantic_weight,
|
|
157
|
+
keyword_weight: keyword_weight,
|
|
158
|
+
)
|
|
142
159
|
end
|
|
143
160
|
|
|
144
161
|
# Parse memory configuration
|
|
@@ -138,7 +138,7 @@ module SwarmMemory
|
|
|
138
138
|
result << "Memory entries matching '#{pattern}' (#{entries.size} #{entries.size == 1 ? "entry" : "entries"}):"
|
|
139
139
|
|
|
140
140
|
entries.each do |entry|
|
|
141
|
-
result << "
|
|
141
|
+
result << "- memory://#{entry[:path]} \"#{entry[:title]}\" (#{format_bytes(entry[:size])})"
|
|
142
142
|
end
|
|
143
143
|
|
|
144
144
|
output = result.join("\n")
|
|
@@ -7,6 +7,8 @@ module SwarmMemory
|
|
|
7
7
|
# Searches content stored in memory entries using regex patterns.
|
|
8
8
|
# Each agent has its own isolated memory storage.
|
|
9
9
|
class MemoryGrep < RubyLLM::Tool
|
|
10
|
+
include TitleLookup
|
|
11
|
+
|
|
10
12
|
description <<~DESC
|
|
11
13
|
Search your memory content using regular expression patterns (like grep).
|
|
12
14
|
|
|
@@ -197,7 +199,7 @@ module SwarmMemory
|
|
|
197
199
|
result = []
|
|
198
200
|
result << "Memory entries matching #{search_desc} (#{paths.size} #{paths.size == 1 ? "entry" : "entries"}):"
|
|
199
201
|
paths.each do |path|
|
|
200
|
-
result << "
|
|
202
|
+
result << "- #{format_memory_path_with_title(path)}"
|
|
201
203
|
end
|
|
202
204
|
result.join("\n")
|
|
203
205
|
end
|
|
@@ -7,8 +7,10 @@ module SwarmMemory
|
|
|
7
7
|
# Retrieves content stored by this agent using memory_write.
|
|
8
8
|
# Each agent has its own isolated memory storage.
|
|
9
9
|
class MemoryRead < RubyLLM::Tool
|
|
10
|
+
include TitleLookup
|
|
11
|
+
|
|
10
12
|
description <<~DESC
|
|
11
|
-
Read content from your memory storage
|
|
13
|
+
Read content from your memory storage.
|
|
12
14
|
|
|
13
15
|
REQUIRED: Provide the file_path parameter - the path to the memory entry you want to read.
|
|
14
16
|
|
|
@@ -25,9 +27,8 @@ module SwarmMemory
|
|
|
25
27
|
INVALID: documentation/, reference/, tutorial/, parallel/, analysis/, notes/
|
|
26
28
|
|
|
27
29
|
**Returns:**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- metadata: All metadata (title, type, tags, tools, permissions, confidence, etc.)
|
|
30
|
+
Markdown content with line numbers (same format as 'cat -n').
|
|
31
|
+
If the entry has related memories, a system-reminder section is appended listing them.
|
|
31
32
|
|
|
32
33
|
**Examples:**
|
|
33
34
|
- MemoryRead(file_path: "concept/ruby/classes.md") - Read a concept
|
|
@@ -38,6 +39,7 @@ module SwarmMemory
|
|
|
38
39
|
- Always read entries before editing them with MemoryEdit
|
|
39
40
|
- Line numbers in output are for reference only - don't include them when editing
|
|
40
41
|
- Each read is tracked to enforce read-before-edit patterns
|
|
42
|
+
- Related memories in the system-reminder can be read with MemoryRead for additional context
|
|
41
43
|
DESC
|
|
42
44
|
|
|
43
45
|
param :file_path,
|
|
@@ -62,7 +64,7 @@ module SwarmMemory
|
|
|
62
64
|
# Execute the tool
|
|
63
65
|
#
|
|
64
66
|
# @param file_path [String] Path to read from
|
|
65
|
-
# @return [String]
|
|
67
|
+
# @return [String] Content with line numbers and optional related memories reminder
|
|
66
68
|
def execute(file_path:)
|
|
67
69
|
# Read full entry with metadata
|
|
68
70
|
entry = @storage.read_entry(file_path: file_path)
|
|
@@ -70,8 +72,14 @@ module SwarmMemory
|
|
|
70
72
|
# Register this read in the tracker with content digest
|
|
71
73
|
Core::StorageReadTracker.register_read(@agent_name, file_path, entry.content)
|
|
72
74
|
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
+
# Return plain text with line numbers
|
|
76
|
+
result = format_with_line_numbers(entry.content)
|
|
77
|
+
|
|
78
|
+
# Append related memories reminder if present
|
|
79
|
+
related_paths = entry.metadata&.dig("related") || []
|
|
80
|
+
result += format_related_memories_reminder(related_paths) if related_paths.any?
|
|
81
|
+
|
|
82
|
+
result
|
|
75
83
|
rescue ArgumentError => e
|
|
76
84
|
validation_error(e.message)
|
|
77
85
|
end
|
|
@@ -82,29 +90,6 @@ module SwarmMemory
|
|
|
82
90
|
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
83
91
|
end
|
|
84
92
|
|
|
85
|
-
# Format entry as JSON with content and metadata
|
|
86
|
-
#
|
|
87
|
-
# Returns a clean JSON format separating content from metadata.
|
|
88
|
-
# This prevents agents from mimicking metadata format when writing.
|
|
89
|
-
#
|
|
90
|
-
# Content includes line numbers (same format as Read tool).
|
|
91
|
-
# Metadata always includes at least title (from Entry).
|
|
92
|
-
# Additional metadata comes from the metadata hash (type, tags, tools, etc.)
|
|
93
|
-
#
|
|
94
|
-
# @param entry [Core::Entry] Entry with content and metadata
|
|
95
|
-
# @return [String] Pretty-printed JSON
|
|
96
|
-
def format_as_json(entry)
|
|
97
|
-
# Build metadata hash with title included
|
|
98
|
-
metadata_hash = { "title" => entry.title }
|
|
99
|
-
metadata_hash.merge!(entry.metadata) if entry.metadata
|
|
100
|
-
|
|
101
|
-
result = {
|
|
102
|
-
content: format_with_line_numbers(entry.content),
|
|
103
|
-
metadata: metadata_hash,
|
|
104
|
-
}
|
|
105
|
-
JSON.pretty_generate(result)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
93
|
# Format content with line numbers (same format as Read tool)
|
|
109
94
|
#
|
|
110
95
|
# @param content [String] Content to format
|
|
@@ -114,10 +99,29 @@ module SwarmMemory
|
|
|
114
99
|
output_lines = lines.each_with_index.map do |line, idx|
|
|
115
100
|
line_number = idx + 1
|
|
116
101
|
display_line = line.chomp
|
|
117
|
-
"#{line_number.to_s.rjust(6)}
|
|
102
|
+
"#{line_number.to_s.rjust(6)} #{display_line}"
|
|
118
103
|
end
|
|
119
104
|
output_lines.join("\n")
|
|
120
105
|
end
|
|
106
|
+
|
|
107
|
+
# Format related memories as a system-reminder section
|
|
108
|
+
#
|
|
109
|
+
# Looks up titles for each related memory path and formats
|
|
110
|
+
# them as a system reminder to help agents discover related content.
|
|
111
|
+
#
|
|
112
|
+
# @param related_paths [Array<String>] Array of memory paths (with or without memory:// prefix)
|
|
113
|
+
# @return [String] Formatted system-reminder section
|
|
114
|
+
def format_related_memories_reminder(related_paths)
|
|
115
|
+
lines = ["\n\n<system-reminder>"]
|
|
116
|
+
lines << "Related memories that may provide additional context:"
|
|
117
|
+
|
|
118
|
+
related_paths.each do |path|
|
|
119
|
+
lines << "- #{format_memory_path_with_title(path)}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
lines << "</system-reminder>"
|
|
123
|
+
lines.join("\n")
|
|
124
|
+
end
|
|
121
125
|
end
|
|
122
126
|
end
|
|
123
127
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmMemory
|
|
4
|
+
module Tools
|
|
5
|
+
# Shared module for looking up memory entry titles
|
|
6
|
+
#
|
|
7
|
+
# Provides a consistent way to look up titles for memory entries
|
|
8
|
+
# across different tools (MemoryRead, MemoryGrep, etc.)
|
|
9
|
+
#
|
|
10
|
+
# @example Including in a tool
|
|
11
|
+
# class MemoryGrep < RubyLLM::Tool
|
|
12
|
+
# include TitleLookup
|
|
13
|
+
#
|
|
14
|
+
# def some_method
|
|
15
|
+
# title = lookup_title("concept/ruby/classes.md")
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
module TitleLookup
|
|
19
|
+
# Look up the title of a memory entry
|
|
20
|
+
#
|
|
21
|
+
# @param path [String] Path to the memory entry
|
|
22
|
+
# @return [String, nil] Title if found, nil otherwise
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# title = lookup_title("concept/ruby/classes.md")
|
|
26
|
+
# # => "Ruby Classes"
|
|
27
|
+
def lookup_title(path)
|
|
28
|
+
entry = @storage.read_entry(file_path: path)
|
|
29
|
+
entry.title
|
|
30
|
+
rescue StandardError
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Format a memory path with its title
|
|
35
|
+
#
|
|
36
|
+
# Normalizes the path (removes memory:// prefix if present) and
|
|
37
|
+
# formats it with the title in quotes if available.
|
|
38
|
+
#
|
|
39
|
+
# @param path [String] Path to the memory entry (with or without memory:// prefix)
|
|
40
|
+
# @return [String] Formatted string like 'memory://path "Title"' or 'memory://path'
|
|
41
|
+
#
|
|
42
|
+
# @example With title found
|
|
43
|
+
# format_memory_path_with_title("concept/ruby/classes.md")
|
|
44
|
+
# # => 'memory://concept/ruby/classes.md "Ruby Classes"'
|
|
45
|
+
#
|
|
46
|
+
# @example With memory:// prefix
|
|
47
|
+
# format_memory_path_with_title("memory://concept/ruby/classes.md")
|
|
48
|
+
# # => 'memory://concept/ruby/classes.md "Ruby Classes"'
|
|
49
|
+
#
|
|
50
|
+
# @example When title not found
|
|
51
|
+
# format_memory_path_with_title("nonexistent.md")
|
|
52
|
+
# # => 'memory://nonexistent.md'
|
|
53
|
+
def format_memory_path_with_title(path)
|
|
54
|
+
normalized_path = path.sub(%r{^memory://}, "")
|
|
55
|
+
title = lookup_title(normalized_path)
|
|
56
|
+
|
|
57
|
+
if title
|
|
58
|
+
"memory://#{normalized_path} \"#{title}\""
|
|
59
|
+
else
|
|
60
|
+
"memory://#{normalized_path}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/swarm_memory/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: swarm_memory
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.2.
|
|
4
|
+
version: 2.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paulo Arruda
|
|
@@ -71,14 +71,14 @@ dependencies:
|
|
|
71
71
|
requirements:
|
|
72
72
|
- - "~>"
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: 2.5.
|
|
74
|
+
version: 2.5.2
|
|
75
75
|
type: :runtime
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: 2.5.
|
|
81
|
+
version: 2.5.2
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
83
|
name: zeitwerk
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -140,14 +140,15 @@ files:
|
|
|
140
140
|
- lib/swarm_memory/tools/memory_grep.rb
|
|
141
141
|
- lib/swarm_memory/tools/memory_read.rb
|
|
142
142
|
- lib/swarm_memory/tools/memory_write.rb
|
|
143
|
+
- lib/swarm_memory/tools/title_lookup.rb
|
|
143
144
|
- lib/swarm_memory/utils.rb
|
|
144
145
|
- lib/swarm_memory/version.rb
|
|
145
|
-
homepage: https://github.com/parruda/
|
|
146
|
+
homepage: https://github.com/parruda/swarm
|
|
146
147
|
licenses:
|
|
147
148
|
- MIT
|
|
148
149
|
metadata:
|
|
149
|
-
source_code_uri: https://github.com/parruda/
|
|
150
|
-
changelog_uri: https://github.com/parruda/
|
|
150
|
+
source_code_uri: https://github.com/parruda/swarm
|
|
151
|
+
changelog_uri: https://github.com/parruda/swarm/blob/main/docs/v2/CHANGELOG.swarm_memory.md
|
|
151
152
|
rdoc_options: []
|
|
152
153
|
require_paths:
|
|
153
154
|
- lib
|