swarm_sdk 2.7.14 → 3.0.0.alpha2
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_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
- data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
- data/lib/swarm_sdk/v3/agent.rb +1165 -0
- data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
- data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
- data/lib/swarm_sdk/v3/configuration.rb +490 -0
- data/lib/swarm_sdk/v3/debug_log.rb +86 -0
- data/lib/swarm_sdk/v3/event_stream.rb +130 -0
- data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
- data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
- data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
- data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
- data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
- data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
- data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
- data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
- data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
- data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
- data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
- data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
- data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
- data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
- data/lib/swarm_sdk/v3/memory/card.rb +206 -0
- data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
- data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
- data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
- data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
- data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
- data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
- data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
- data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
- data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
- data/lib/swarm_sdk/v3/memory/store.rb +489 -0
- data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
- data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
- data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
- data/lib/swarm_sdk/v3/tools/base.rb +80 -0
- data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
- data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
- data/lib/swarm_sdk/v3/tools/document_converters/base.rb +84 -0
- data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
- data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
- data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -0
- data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
- data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
- data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
- data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
- data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
- data/lib/swarm_sdk/v3/tools/read.rb +213 -0
- data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
- data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
- data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
- data/lib/swarm_sdk/v3/tools/think.rb +88 -0
- data/lib/swarm_sdk/v3/tools/write.rb +87 -0
- data/lib/swarm_sdk/v3.rb +145 -0
- metadata +88 -149
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
- data/lib/swarm_sdk/agent/builder.rb +0 -705
- data/lib/swarm_sdk/agent/chat.rb +0 -1438
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
- data/lib/swarm_sdk/agent/context.rb +0 -115
- data/lib/swarm_sdk/agent/context_manager.rb +0 -315
- data/lib/swarm_sdk/agent/definition.rb +0 -588
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
- data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
- data/lib/swarm_sdk/agent_registry.rb +0 -146
- data/lib/swarm_sdk/builders/base_builder.rb +0 -558
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
- data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
- data/lib/swarm_sdk/concerns/validatable.rb +0 -55
- data/lib/swarm_sdk/config.rb +0 -368
- data/lib/swarm_sdk/configuration/parser.rb +0 -397
- data/lib/swarm_sdk/configuration/translator.rb +0 -285
- data/lib/swarm_sdk/configuration.rb +0 -165
- data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
- data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
- data/lib/swarm_sdk/context_compactor.rb +0 -335
- data/lib/swarm_sdk/context_management/builder.rb +0 -128
- data/lib/swarm_sdk/context_management/context.rb +0 -328
- data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
- data/lib/swarm_sdk/defaults.rb +0 -251
- data/lib/swarm_sdk/events_to_messages.rb +0 -199
- data/lib/swarm_sdk/hooks/adapter.rb +0 -359
- data/lib/swarm_sdk/hooks/context.rb +0 -197
- data/lib/swarm_sdk/hooks/definition.rb +0 -80
- data/lib/swarm_sdk/hooks/error.rb +0 -29
- data/lib/swarm_sdk/hooks/executor.rb +0 -146
- data/lib/swarm_sdk/hooks/registry.rb +0 -147
- data/lib/swarm_sdk/hooks/result.rb +0 -150
- data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
- data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
- data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
- data/lib/swarm_sdk/log_collector.rb +0 -227
- data/lib/swarm_sdk/log_stream.rb +0 -127
- data/lib/swarm_sdk/markdown_parser.rb +0 -75
- data/lib/swarm_sdk/model_aliases.json +0 -8
- data/lib/swarm_sdk/models.json +0 -44002
- data/lib/swarm_sdk/models.rb +0 -161
- data/lib/swarm_sdk/node_context.rb +0 -245
- data/lib/swarm_sdk/observer/builder.rb +0 -81
- data/lib/swarm_sdk/observer/config.rb +0 -45
- data/lib/swarm_sdk/observer/manager.rb +0 -248
- data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
- data/lib/swarm_sdk/permissions/config.rb +0 -239
- data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
- data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
- data/lib/swarm_sdk/permissions/validator.rb +0 -173
- data/lib/swarm_sdk/permissions_builder.rb +0 -122
- data/lib/swarm_sdk/plugin.rb +0 -309
- data/lib/swarm_sdk/plugin_registry.rb +0 -101
- data/lib/swarm_sdk/proc_helpers.rb +0 -53
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -241
- data/lib/swarm_sdk/snapshot.rb +0 -156
- data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
- data/lib/swarm_sdk/state_restorer.rb +0 -476
- data/lib/swarm_sdk/state_snapshot.rb +0 -334
- data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
- data/lib/swarm_sdk/swarm/builder.rb +0 -256
- data/lib/swarm_sdk/swarm/executor.rb +0 -446
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
- data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
- data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
- data/lib/swarm_sdk/swarm.rb +0 -973
- data/lib/swarm_sdk/swarm_loader.rb +0 -145
- data/lib/swarm_sdk/swarm_registry.rb +0 -136
- data/lib/swarm_sdk/tools/base.rb +0 -63
- data/lib/swarm_sdk/tools/bash.rb +0 -280
- data/lib/swarm_sdk/tools/clock.rb +0 -46
- data/lib/swarm_sdk/tools/delegate.rb +0 -389
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
- data/lib/swarm_sdk/tools/edit.rb +0 -145
- data/lib/swarm_sdk/tools/glob.rb +0 -166
- data/lib/swarm_sdk/tools/grep.rb +0 -235
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
- data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
- data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
- data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
- data/lib/swarm_sdk/tools/read.rb +0 -261
- data/lib/swarm_sdk/tools/registry.rb +0 -205
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
- data/lib/swarm_sdk/tools/think.rb +0 -100
- data/lib/swarm_sdk/tools/todo_write.rb +0 -237
- data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
- data/lib/swarm_sdk/tools/write.rb +0 -112
- data/lib/swarm_sdk/transcript_builder.rb +0 -278
- data/lib/swarm_sdk/utils.rb +0 -68
- data/lib/swarm_sdk/validation_result.rb +0 -33
- data/lib/swarm_sdk/version.rb +0 -5
- data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
- data/lib/swarm_sdk/workflow/builder.rb +0 -227
- data/lib/swarm_sdk/workflow/executor.rb +0 -497
- data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
- data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
- data/lib/swarm_sdk/workflow.rb +0 -589
- data/lib/swarm_sdk.rb +0 -721
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Memory
|
|
6
|
+
module Adapters
|
|
7
|
+
# Shared FAISS vector index support for adapters
|
|
8
|
+
#
|
|
9
|
+
# Provides FAISS-based vector search and index management that can
|
|
10
|
+
# be included by any adapter that stores embeddings as Ruby arrays.
|
|
11
|
+
# The FAISS index (IndexFlatIP with L2-normalized vectors) handles
|
|
12
|
+
# efficient top-k nearest neighbor search.
|
|
13
|
+
#
|
|
14
|
+
# ## Requirements for including classes
|
|
15
|
+
#
|
|
16
|
+
# The including class must:
|
|
17
|
+
# - Call {#initialize_faiss!} from its constructor
|
|
18
|
+
# - Define a `faiss_directory` method returning the path for FAISS files
|
|
19
|
+
# - Implement `list_cards` (used by {#rebuild_index})
|
|
20
|
+
#
|
|
21
|
+
# @example Including in a custom adapter
|
|
22
|
+
# class MyAdapter < Base
|
|
23
|
+
# include VectorUtils # pairwise similarity
|
|
24
|
+
# include FaissSupport # FAISS vector index
|
|
25
|
+
#
|
|
26
|
+
# def initialize(directory)
|
|
27
|
+
# super()
|
|
28
|
+
# @directory = directory
|
|
29
|
+
# initialize_faiss!
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# private
|
|
33
|
+
#
|
|
34
|
+
# def faiss_directory
|
|
35
|
+
# @directory
|
|
36
|
+
# end
|
|
37
|
+
# end
|
|
38
|
+
module FaissSupport
|
|
39
|
+
# Embedding dimensions for MiniLM-L6-v2 model
|
|
40
|
+
EMBEDDING_DIMENSIONS = 384
|
|
41
|
+
|
|
42
|
+
# Search the FAISS index for similar vectors
|
|
43
|
+
#
|
|
44
|
+
# @param embedding [Array<Float>] Query embedding
|
|
45
|
+
# @param top_k [Integer] Maximum number of results
|
|
46
|
+
# @param threshold [Float] Minimum cosine similarity to include
|
|
47
|
+
# @return [Array<Hash>] Array of `{ id: String, similarity: Float }`
|
|
48
|
+
def vector_search(embedding, top_k:, threshold: 0.0)
|
|
49
|
+
return [] unless @faiss_index && @faiss_index.ntotal > 0
|
|
50
|
+
|
|
51
|
+
normalized = normalize_vector(embedding)
|
|
52
|
+
effective_k = [top_k, @faiss_index.ntotal].min
|
|
53
|
+
# Explicit Numo conversion for safe FAISS C extension boundary crossing
|
|
54
|
+
query_vector = Numo::SFloat.cast([normalized])
|
|
55
|
+
distances, ids = @faiss_index.search(query_vector, effective_k)
|
|
56
|
+
|
|
57
|
+
index_to_id = @id_to_index.invert
|
|
58
|
+
# FAISS returns Numo::NArray — convert to Ruby arrays
|
|
59
|
+
id_row = ids.to_a.flatten
|
|
60
|
+
dist_row = distances.to_a.flatten
|
|
61
|
+
results = []
|
|
62
|
+
id_row.each_with_index do |idx, i|
|
|
63
|
+
next if idx < 0
|
|
64
|
+
|
|
65
|
+
card_id = index_to_id[idx]
|
|
66
|
+
next unless card_id
|
|
67
|
+
|
|
68
|
+
similarity = dist_row[i]
|
|
69
|
+
next if similarity < threshold
|
|
70
|
+
|
|
71
|
+
results << { id: card_id, similarity: similarity }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
results
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Rebuild the FAISS index from all stored cards
|
|
78
|
+
#
|
|
79
|
+
# @return [void]
|
|
80
|
+
def rebuild_index
|
|
81
|
+
@faiss_index = create_faiss_index
|
|
82
|
+
@id_to_index = {}
|
|
83
|
+
@next_index_id = 0
|
|
84
|
+
|
|
85
|
+
list_cards.each do |card|
|
|
86
|
+
add_to_vector_index(card) if card.embedding
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# Initialize FAISS instance variables
|
|
93
|
+
#
|
|
94
|
+
# Must be called from the including class's constructor.
|
|
95
|
+
#
|
|
96
|
+
# @return [void]
|
|
97
|
+
def initialize_faiss!
|
|
98
|
+
@id_to_index = {}
|
|
99
|
+
@next_index_id = 0
|
|
100
|
+
@faiss_index = nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Create a new FAISS IndexIDMap backed by IndexFlatIP
|
|
104
|
+
#
|
|
105
|
+
# @return [Faiss::IndexIDMap] New FAISS index
|
|
106
|
+
def create_faiss_index
|
|
107
|
+
require "faiss"
|
|
108
|
+
# Keep a reference to the inner index to prevent GC from collecting
|
|
109
|
+
# it while IndexIDMap holds a raw C pointer to it. Without this,
|
|
110
|
+
# Ruby's GC can free the inner index, leaving IndexIDMap with a
|
|
111
|
+
# dangling pointer that causes segfaults on add/search.
|
|
112
|
+
@faiss_inner_index = Faiss::IndexFlatIP.new(EMBEDDING_DIMENSIONS)
|
|
113
|
+
Faiss::IndexIDMap.new(@faiss_inner_index)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Add a card's embedding to the FAISS index
|
|
117
|
+
#
|
|
118
|
+
# @param card [Card] Card with embedding
|
|
119
|
+
# @return [void]
|
|
120
|
+
def add_to_vector_index(card)
|
|
121
|
+
@faiss_index ||= create_faiss_index
|
|
122
|
+
normalized = normalize_vector(card.embedding)
|
|
123
|
+
index_id = @next_index_id
|
|
124
|
+
@next_index_id += 1
|
|
125
|
+
# Explicit Numo conversion avoids segfaults from implicit
|
|
126
|
+
# Ruby Array → Numo cast at the FAISS C extension boundary.
|
|
127
|
+
vectors = Numo::SFloat.cast([normalized])
|
|
128
|
+
ids = Numo::Int64.cast([index_id])
|
|
129
|
+
@faiss_index.add_with_ids(vectors, ids)
|
|
130
|
+
@id_to_index[card.id] = index_id
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Remove a card from the vector index
|
|
134
|
+
#
|
|
135
|
+
# Removes the ID mapping. Full FAISS rebuild needed for actual removal.
|
|
136
|
+
#
|
|
137
|
+
# @param card_id [String] Card ID
|
|
138
|
+
# @return [void]
|
|
139
|
+
def remove_from_vector_index(card_id)
|
|
140
|
+
@id_to_index.delete(card_id)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# L2-normalize a vector for cosine similarity via inner product
|
|
144
|
+
#
|
|
145
|
+
# @param vec [Array<Float>] Input vector
|
|
146
|
+
# @return [Array<Float>] Normalized vector
|
|
147
|
+
def normalize_vector(vec)
|
|
148
|
+
magnitude = Math.sqrt(vec.sum { |v| v * v })
|
|
149
|
+
return vec if magnitude.zero?
|
|
150
|
+
|
|
151
|
+
vec.map { |v| v / magnitude }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Save the FAISS index and ID mapping to disk
|
|
155
|
+
#
|
|
156
|
+
# @return [void]
|
|
157
|
+
def save_faiss_index
|
|
158
|
+
return unless @faiss_index
|
|
159
|
+
|
|
160
|
+
path = File.join(faiss_directory, "index.faiss")
|
|
161
|
+
@faiss_index.save(path)
|
|
162
|
+
|
|
163
|
+
# Save ID mapping separately
|
|
164
|
+
mapping_path = File.join(faiss_directory, "index_mapping.json")
|
|
165
|
+
File.write(mapping_path, JSON.generate(
|
|
166
|
+
id_to_index: @id_to_index,
|
|
167
|
+
next_index_id: @next_index_id,
|
|
168
|
+
))
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Load the FAISS index and ID mapping from disk
|
|
172
|
+
#
|
|
173
|
+
# @return [void]
|
|
174
|
+
def load_faiss_index
|
|
175
|
+
path = File.join(faiss_directory, "index.faiss")
|
|
176
|
+
mapping_path = File.join(faiss_directory, "index_mapping.json")
|
|
177
|
+
|
|
178
|
+
if File.exist?(path) && File.exist?(mapping_path)
|
|
179
|
+
require "faiss"
|
|
180
|
+
@faiss_index = Faiss::Index.load(path)
|
|
181
|
+
mapping = JSON.parse(File.read(mapping_path))
|
|
182
|
+
@id_to_index = mapping["id_to_index"] || {}
|
|
183
|
+
@next_index_id = mapping["next_index_id"] || 0
|
|
184
|
+
else
|
|
185
|
+
@faiss_index = nil
|
|
186
|
+
@id_to_index = {}
|
|
187
|
+
@next_index_id = 0
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Memory
|
|
6
|
+
module Adapters
|
|
7
|
+
# Filesystem-based storage adapter with FAISS vector index
|
|
8
|
+
#
|
|
9
|
+
# Stores cards as individual JSON files, edges and clusters in
|
|
10
|
+
# aggregate JSON files, and vectors in a FAISS IndexFlatIP index
|
|
11
|
+
# (cosine similarity via L2-normalized vectors + inner product).
|
|
12
|
+
#
|
|
13
|
+
# Directory structure:
|
|
14
|
+
# .swarm/memory/
|
|
15
|
+
# ├── cards/
|
|
16
|
+
# │ ├── card_a1b2c3.json
|
|
17
|
+
# │ └── ...
|
|
18
|
+
# ├── edges.json
|
|
19
|
+
# ├── clusters.json
|
|
20
|
+
# └── index.faiss
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# adapter = FilesystemAdapter.new("/path/to/.swarm/memory")
|
|
24
|
+
# adapter.load
|
|
25
|
+
# adapter.write_card(card)
|
|
26
|
+
# adapter.save
|
|
27
|
+
class FilesystemAdapter < Base
|
|
28
|
+
include VectorUtils
|
|
29
|
+
include FaissSupport
|
|
30
|
+
|
|
31
|
+
# @return [String] Root directory for storage
|
|
32
|
+
attr_reader :directory
|
|
33
|
+
|
|
34
|
+
# @param directory [String] Root directory for memory storage
|
|
35
|
+
def initialize(directory)
|
|
36
|
+
super()
|
|
37
|
+
@directory = File.expand_path(directory)
|
|
38
|
+
@edges = []
|
|
39
|
+
@clusters = []
|
|
40
|
+
initialize_faiss!
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# --- Card CRUD ---
|
|
44
|
+
|
|
45
|
+
# @param card [Card] Card to write
|
|
46
|
+
# @return [void]
|
|
47
|
+
def write_card(card)
|
|
48
|
+
ensure_directories!
|
|
49
|
+
path = card_path(card.id)
|
|
50
|
+
File.write(path, JSON.pretty_generate(card.to_h))
|
|
51
|
+
add_to_vector_index(card) if card.embedding
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param id [String] Card ID
|
|
55
|
+
# @return [Card, nil]
|
|
56
|
+
def read_card(id)
|
|
57
|
+
path = card_path(id)
|
|
58
|
+
return unless File.exist?(path)
|
|
59
|
+
|
|
60
|
+
data = JSON.parse(File.read(path))
|
|
61
|
+
Card.from_h(data)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param id [String] Card ID
|
|
65
|
+
# @return [void]
|
|
66
|
+
def delete_card(id)
|
|
67
|
+
path = card_path(id)
|
|
68
|
+
File.delete(path) if File.exist?(path)
|
|
69
|
+
remove_from_vector_index(id)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @param prefix [String, nil] ID prefix filter
|
|
73
|
+
# @return [Array<Card>]
|
|
74
|
+
def list_cards(prefix: nil)
|
|
75
|
+
ensure_directories!
|
|
76
|
+
pattern = prefix ? "#{prefix}*.json" : "*.json"
|
|
77
|
+
Dir.glob(File.join(cards_dir, pattern)).map do |path|
|
|
78
|
+
data = JSON.parse(File.read(path))
|
|
79
|
+
Card.from_h(data)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# --- Edge CRUD ---
|
|
84
|
+
|
|
85
|
+
# @param edge [Edge] Edge to write
|
|
86
|
+
# @return [void]
|
|
87
|
+
def write_edge(edge)
|
|
88
|
+
@edges << edge
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @param card_id [String] Card ID
|
|
92
|
+
# @param type [Symbol, nil] Filter by edge type
|
|
93
|
+
# @return [Array<Edge>]
|
|
94
|
+
def edges_for(card_id, type: nil)
|
|
95
|
+
results = @edges.select { |e| e.from_id == card_id || e.to_id == card_id }
|
|
96
|
+
results = results.select { |e| e.type == type } if type
|
|
97
|
+
results
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @param card_id [String] Card ID
|
|
101
|
+
# @return [void]
|
|
102
|
+
def delete_edges_for(card_id)
|
|
103
|
+
@edges.reject! { |e| e.from_id == card_id || e.to_id == card_id }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# --- Cluster CRUD ---
|
|
107
|
+
|
|
108
|
+
# @param cluster [Cluster] Cluster to write
|
|
109
|
+
# @return [void]
|
|
110
|
+
def write_cluster(cluster)
|
|
111
|
+
idx = @clusters.index { |c| c.id == cluster.id }
|
|
112
|
+
if idx
|
|
113
|
+
@clusters[idx] = cluster
|
|
114
|
+
else
|
|
115
|
+
@clusters << cluster
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @param id [String] Cluster ID
|
|
120
|
+
# @return [Cluster, nil]
|
|
121
|
+
def read_cluster(id)
|
|
122
|
+
@clusters.find { |c| c.id == id }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @return [Array<Cluster>]
|
|
126
|
+
def list_clusters
|
|
127
|
+
@clusters.dup
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# --- Persistence ---
|
|
131
|
+
|
|
132
|
+
# Save edges, clusters, and FAISS index to disk
|
|
133
|
+
#
|
|
134
|
+
# @return [void]
|
|
135
|
+
def save
|
|
136
|
+
ensure_directories!
|
|
137
|
+
save_edges
|
|
138
|
+
save_clusters
|
|
139
|
+
save_faiss_index
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Load edges, clusters, and FAISS index from disk
|
|
143
|
+
#
|
|
144
|
+
# @return [void]
|
|
145
|
+
def load
|
|
146
|
+
ensure_directories!
|
|
147
|
+
load_edges
|
|
148
|
+
load_clusters
|
|
149
|
+
load_faiss_index
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
# @return [String] Path to the FAISS index directory
|
|
155
|
+
def faiss_directory
|
|
156
|
+
@directory
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# @return [String] Path to cards directory
|
|
160
|
+
def cards_dir
|
|
161
|
+
File.join(@directory, "cards")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# @param id [String] Card ID
|
|
165
|
+
# @return [String] Path to card JSON file
|
|
166
|
+
def card_path(id)
|
|
167
|
+
File.join(cards_dir, "#{id}.json")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Ensure required directories exist
|
|
171
|
+
#
|
|
172
|
+
# @return [void]
|
|
173
|
+
def ensure_directories!
|
|
174
|
+
FileUtils.mkdir_p(cards_dir)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# --- Serialization ---
|
|
178
|
+
|
|
179
|
+
# @return [void]
|
|
180
|
+
def save_edges
|
|
181
|
+
path = File.join(@directory, "edges.json")
|
|
182
|
+
File.write(path, JSON.pretty_generate(@edges.map(&:to_h)))
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# @return [void]
|
|
186
|
+
def load_edges
|
|
187
|
+
path = File.join(@directory, "edges.json")
|
|
188
|
+
return unless File.exist?(path)
|
|
189
|
+
|
|
190
|
+
data = JSON.parse(File.read(path))
|
|
191
|
+
@edges = data.map { |h| Edge.from_h(h) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# @return [void]
|
|
195
|
+
def save_clusters
|
|
196
|
+
path = File.join(@directory, "clusters.json")
|
|
197
|
+
File.write(path, JSON.pretty_generate(@clusters.map(&:to_h)))
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# @return [void]
|
|
201
|
+
def load_clusters
|
|
202
|
+
path = File.join(@directory, "clusters.json")
|
|
203
|
+
return unless File.exist?(path)
|
|
204
|
+
|
|
205
|
+
data = JSON.parse(File.read(path))
|
|
206
|
+
@clusters = data.map { |h| Cluster.from_h(h) }
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|