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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4216c2f9b10d9accb547d94709305359009d8b8a7440c3023c97d49cc66b5cd4
4
- data.tar.gz: 670b1af5ec7880caa86fbceed37a030537cd863ec6a32155c144f706f1f96172
3
+ metadata.gz: 768a8a8cd1d0ab5a3edff5e6e0c54e3fdce1e6c6af281d6a39285019167b87c8
4
+ data.tar.gz: aa9e396d3c72e53bf49459828c635148205c523e6d16bb42e7f6e4e5856a1d4c
5
5
  SHA512:
6
- metadata.gz: 747e338d2569036d590c783c674895fdb9420af52655e34a412605f4085ced7c0bc198bd70698df2c84689211846095e381622f6bbb139376c70e3d71b3e6871
7
- data.tar.gz: bf9c87e64f5e149d7dbb00157abd6ba39f005e16cf29304382235c1ede975a13a76d1afc72ce12e83dfcec072610536a53e7abc457d8e0a7dff0061892e29759
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
- # Hybrid score: weighted combination
193
- hybrid_score = (@semantic_weight * semantic_score) + (@keyword_weight * keyword_score)
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
- def initialize(adapter:, embedder: nil)
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
- SemanticIndex.new(adapter: adapter, embedder: embedder)
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| k == :adapter || k == "adapter" || k == :mode || k == "mode" }
116
- [adapter, options]
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
- # Instantiate adapter with options
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 (enables semantic features)
141
- Core::Storage.new(adapter: adapter, embedder: embedder)
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 << " memory://#{entry[:path]} - \"#{entry[:title]}\" (#{format_bytes(entry[:size])})"
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 << " memory://#{path}"
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 and retrieve all associated metadata.
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
- JSON with two fields:
29
- - content: Markdown content with line numbers (same format as Read tool)
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] JSON with content and metadata
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
- # Always return JSON format (metadata always exists - at minimum title)
74
- format_as_json(entry)
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)}→#{display_line}"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmMemory
4
- VERSION = "2.2.2"
4
+ VERSION = "2.2.4"
5
5
  end
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.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.1
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.1
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/claude-swarm
146
+ homepage: https://github.com/parruda/swarm
146
147
  licenses:
147
148
  - MIT
148
149
  metadata:
149
- source_code_uri: https://github.com/parruda/claude-swarm
150
- changelog_uri: https://github.com/parruda/claude-swarm/blob/main/docs/v2/CHANGELOG.swarm_memory.md
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