swarm_sdk 2.7.14 → 3.0.0.alpha1
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/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 +181 -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 +83 -148
- 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,496 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Memory
|
|
6
|
+
# Compression ladder for memory cards
|
|
7
|
+
#
|
|
8
|
+
# Demotes low-exposure cards through compression levels:
|
|
9
|
+
# - L0: Raw (original text)
|
|
10
|
+
# - L1: Faithful summary
|
|
11
|
+
# - L2: Sparse bullets
|
|
12
|
+
# - L3: Schema-only (entities + type)
|
|
13
|
+
# - L4: Embedding + title only
|
|
14
|
+
#
|
|
15
|
+
# Also supports **promotion**: when a compressed card (L1-L4) is
|
|
16
|
+
# accessed frequently, it gets promoted one level up by rebuilding
|
|
17
|
+
# a richer summary from its graph neighbors and cluster context.
|
|
18
|
+
#
|
|
19
|
+
# Uses LLM for L0→L1 and L1→L2 transitions. L3 and L4 are
|
|
20
|
+
# mechanical transformations that don't require LLM calls.
|
|
21
|
+
#
|
|
22
|
+
# @example Compress
|
|
23
|
+
# compressor = Compressor.new(adapter: adapter, chat: llm_chat)
|
|
24
|
+
# compressor.compress_card(card) # Advances one level
|
|
25
|
+
#
|
|
26
|
+
# @example Promote
|
|
27
|
+
# compressor.promote_card(card) # Moves one level up if eligible
|
|
28
|
+
class Compressor
|
|
29
|
+
# Maximum cards per LLM call to stay within token limits
|
|
30
|
+
BATCH_SIZE = 20
|
|
31
|
+
|
|
32
|
+
# @param adapter [Adapters::Base] Storage adapter
|
|
33
|
+
# @param chat [RubyLLM::Chat, nil] LLM chat for summarization (nil = skip LLM steps)
|
|
34
|
+
def initialize(adapter:, chat: nil)
|
|
35
|
+
@adapter = adapter
|
|
36
|
+
@chat = chat
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Compress a card one level down the ladder
|
|
40
|
+
#
|
|
41
|
+
# @param card [Card] Card to compress
|
|
42
|
+
# @return [Card] Updated card (also persisted to adapter)
|
|
43
|
+
def compress_card(card)
|
|
44
|
+
case card.compression_level
|
|
45
|
+
when 0 then compress_l0_to_l1(card)
|
|
46
|
+
when 1 then compress_l1_to_l2(card)
|
|
47
|
+
when 2 then compress_l2_to_l3(card)
|
|
48
|
+
when 3 then compress_l3_to_l4(card)
|
|
49
|
+
else card # Already at max compression
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Compress all low-exposure cards using batched LLM calls
|
|
54
|
+
#
|
|
55
|
+
# Groups cards by compression level transition and batches LLM calls
|
|
56
|
+
# for L0→L1 and L1→L2. L2→L3 and L3→L4 are mechanical (no LLM).
|
|
57
|
+
#
|
|
58
|
+
# @param exposure_tracker [ExposureTracker] For scoring cards
|
|
59
|
+
# @param threshold [Float] Exposure score threshold
|
|
60
|
+
# @return [Integer] Number of cards compressed
|
|
61
|
+
def compress_low_exposure(exposure_tracker:, threshold: 1.0)
|
|
62
|
+
candidates = exposure_tracker.low_exposure_cards(threshold: threshold)
|
|
63
|
+
eligible = candidates.reject { |c| c.compression_level >= 4 }
|
|
64
|
+
DebugLog.log("compressor", "compress_low_exposure: #{eligible.size}/#{candidates.size} eligible cards")
|
|
65
|
+
return 0 if eligible.empty?
|
|
66
|
+
|
|
67
|
+
# Group by current compression level
|
|
68
|
+
l0_cards = eligible.select { |c| c.compression_level == 0 }
|
|
69
|
+
l1_cards = eligible.select { |c| c.compression_level == 1 }
|
|
70
|
+
l2_cards = eligible.select { |c| c.compression_level == 2 }
|
|
71
|
+
l3_cards = eligible.select { |c| c.compression_level == 3 }
|
|
72
|
+
|
|
73
|
+
count = 0
|
|
74
|
+
|
|
75
|
+
# Batch LLM transitions (L0→L1, L1→L2)
|
|
76
|
+
count += batch_compress_l0_to_l1(l0_cards) if l0_cards.any?
|
|
77
|
+
count += batch_compress_l1_to_l2(l1_cards) if l1_cards.any?
|
|
78
|
+
|
|
79
|
+
# Mechanical transitions (no LLM needed)
|
|
80
|
+
l2_cards.each { |c| compress_l2_to_l3(c) }
|
|
81
|
+
count += l2_cards.size
|
|
82
|
+
|
|
83
|
+
l3_cards.each { |c| compress_l3_to_l4(c) }
|
|
84
|
+
count += l3_cards.size
|
|
85
|
+
|
|
86
|
+
DebugLog.log("compressor", "compressed #{count} cards")
|
|
87
|
+
count
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Promote a compressed card one level up
|
|
91
|
+
#
|
|
92
|
+
# When a compressed card is accessed frequently, it should be promoted
|
|
93
|
+
# to recover detail. L4→L3 and L3→L2 are mechanical (rebuild from
|
|
94
|
+
# entities/type). L2→L1 and L1→L0 use LLM to rebuild richer text
|
|
95
|
+
# from graph neighbors and cluster context.
|
|
96
|
+
#
|
|
97
|
+
# @param card [Card] Compressed card (compression_level > 0)
|
|
98
|
+
# @return [Card] Updated card at one level higher, or unchanged if L0
|
|
99
|
+
def promote_card(card)
|
|
100
|
+
case card.compression_level
|
|
101
|
+
when 4 then promote_l4_to_l3(card)
|
|
102
|
+
when 3 then promote_l3_to_l2(card)
|
|
103
|
+
when 2 then promote_l2_to_l1(card)
|
|
104
|
+
when 1 then promote_l1_to_l0(card)
|
|
105
|
+
else card # Already at L0
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Promote all eligible compressed cards that have high access
|
|
110
|
+
#
|
|
111
|
+
# Cards at L1+ with access_count above the threshold are promoted
|
|
112
|
+
# one level toward L0. Uses batched LLM calls for L2→L1 and L1→L0
|
|
113
|
+
# promotions.
|
|
114
|
+
#
|
|
115
|
+
# @param access_threshold [Integer] Minimum access count for promotion
|
|
116
|
+
# @return [Integer] Number of cards promoted
|
|
117
|
+
def promote_eligible(access_threshold: 5)
|
|
118
|
+
eligible = @adapter.list_cards.select do |card|
|
|
119
|
+
card.compression_level >= 1 && card.access_count >= access_threshold
|
|
120
|
+
end
|
|
121
|
+
return 0 if eligible.empty?
|
|
122
|
+
|
|
123
|
+
# Group by current compression level
|
|
124
|
+
l4_cards = eligible.select { |c| c.compression_level == 4 }
|
|
125
|
+
l3_cards = eligible.select { |c| c.compression_level == 3 }
|
|
126
|
+
l2_cards = eligible.select { |c| c.compression_level == 2 }
|
|
127
|
+
l1_cards = eligible.select { |c| c.compression_level == 1 }
|
|
128
|
+
|
|
129
|
+
count = 0
|
|
130
|
+
|
|
131
|
+
# Mechanical promotions (L4→L3, L3→L2)
|
|
132
|
+
l4_cards.each { |c| promote_l4_to_l3(c) }
|
|
133
|
+
count += l4_cards.size
|
|
134
|
+
|
|
135
|
+
l3_cards.each { |c| promote_l3_to_l2(c) }
|
|
136
|
+
count += l3_cards.size
|
|
137
|
+
|
|
138
|
+
# Batch LLM promotions (L2→L1, L1→L0)
|
|
139
|
+
count += batch_promote_l2_to_l1(l2_cards) if l2_cards.any?
|
|
140
|
+
count += batch_promote_l1_to_l0(l1_cards) if l1_cards.any?
|
|
141
|
+
|
|
142
|
+
count
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
# Batch compress L0 cards to L1 using a single LLM call per batch
|
|
148
|
+
#
|
|
149
|
+
# @param cards [Array<Card>] L0 cards to compress
|
|
150
|
+
# @return [Integer] Number of cards compressed
|
|
151
|
+
def batch_compress_l0_to_l1(cards)
|
|
152
|
+
batch_llm_compress(cards, 1, "Summarize each card faithfully in 1-2 sentences.")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Batch compress L1 cards to L2 using a single LLM call per batch
|
|
156
|
+
#
|
|
157
|
+
# @param cards [Array<Card>] L1 cards to compress
|
|
158
|
+
# @return [Integer] Number of cards compressed
|
|
159
|
+
def batch_compress_l1_to_l2(cards)
|
|
160
|
+
batch_llm_compress(cards, 2, "Convert each card to 2-3 bullet points.")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Batch compress cards using XML-tagged LLM calls
|
|
164
|
+
#
|
|
165
|
+
# Groups cards into batches of BATCH_SIZE and makes one LLM call per
|
|
166
|
+
# batch. Falls back to individual compression for any cards missing
|
|
167
|
+
# from the batch response.
|
|
168
|
+
#
|
|
169
|
+
# @param cards [Array<Card>] Cards to compress
|
|
170
|
+
# @param target_level [Integer] Target compression level
|
|
171
|
+
# @param task_description [String] LLM instruction
|
|
172
|
+
# @return [Integer] Number of cards compressed
|
|
173
|
+
def batch_llm_compress(cards, target_level, task_description)
|
|
174
|
+
return cards.each { |c| compress_card(c) }.size unless @chat
|
|
175
|
+
|
|
176
|
+
count = 0
|
|
177
|
+
cards.each_slice(BATCH_SIZE) do |batch|
|
|
178
|
+
results = llm_batch_summarize(batch, task_description)
|
|
179
|
+
|
|
180
|
+
batch.each_with_index do |card, i|
|
|
181
|
+
summary = results[i + 1] # 1-indexed IDs
|
|
182
|
+
if summary
|
|
183
|
+
card.text = summary
|
|
184
|
+
card.compression_level = target_level
|
|
185
|
+
card.updated_at = Time.now
|
|
186
|
+
@adapter.write_card(card)
|
|
187
|
+
else
|
|
188
|
+
# Missing from batch response — fall back to individual
|
|
189
|
+
compress_card(card)
|
|
190
|
+
end
|
|
191
|
+
count += 1
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
count
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Make a batched LLM call with XML-tagged cards
|
|
198
|
+
#
|
|
199
|
+
# @param cards [Array<Card>] Cards to summarize
|
|
200
|
+
# @param task_description [String] LLM instruction
|
|
201
|
+
# @return [Hash<Integer, String>] Mapping of card ID (1-indexed) to content
|
|
202
|
+
def llm_batch_summarize(cards, task_description)
|
|
203
|
+
@chat.reset_messages!
|
|
204
|
+
|
|
205
|
+
input_cards = cards.each_with_index.map do |card, i|
|
|
206
|
+
"<card id=\"#{i + 1}\">\n#{card.text}\n</card>"
|
|
207
|
+
end.join("\n\n")
|
|
208
|
+
|
|
209
|
+
prompt = <<~PROMPT
|
|
210
|
+
#{task_description}
|
|
211
|
+
|
|
212
|
+
#{input_cards}
|
|
213
|
+
|
|
214
|
+
Return each result inside <card id="N">RESULT</card> tags matching the input IDs.
|
|
215
|
+
PROMPT
|
|
216
|
+
|
|
217
|
+
response = @chat.ask(prompt)
|
|
218
|
+
parse_card_responses(response.content)
|
|
219
|
+
rescue StandardError => e
|
|
220
|
+
EventStream.emit(type: "memory_compression_batch_error", error: "#{e.class}: #{e.message}")
|
|
221
|
+
{} # Return empty hash to trigger individual fallback
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Parse <card id="N">content</card> tags from LLM response
|
|
225
|
+
#
|
|
226
|
+
# @param text [String] LLM response text
|
|
227
|
+
# @return [Hash<Integer, String>] Mapping of card ID to content
|
|
228
|
+
def parse_card_responses(text)
|
|
229
|
+
results = {}
|
|
230
|
+
text.scan(%r{<card\s+id="(\d+)">(.*?)</card>}mi) do |id, content|
|
|
231
|
+
results[id.to_i] = content.strip
|
|
232
|
+
end
|
|
233
|
+
results
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Batch promote L2 cards to L1 using LLM
|
|
237
|
+
#
|
|
238
|
+
# @param cards [Array<Card>] L2 cards to promote
|
|
239
|
+
# @return [Integer] Number of cards promoted
|
|
240
|
+
def batch_promote_l2_to_l1(cards)
|
|
241
|
+
batch_llm_promote(cards, 1, "Write a faithful 1-2 sentence summary for each card:")
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Batch promote L1 cards to L0 using LLM
|
|
245
|
+
#
|
|
246
|
+
# @param cards [Array<Card>] L1 cards to promote
|
|
247
|
+
# @return [Integer] Number of cards promoted
|
|
248
|
+
def batch_promote_l1_to_l0(cards)
|
|
249
|
+
batch_llm_promote(cards, 0, "Expand each card into a detailed paragraph (max 250 words):")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Batch promote cards using XML-tagged LLM calls with neighbor context
|
|
253
|
+
#
|
|
254
|
+
# @param cards [Array<Card>] Cards to promote
|
|
255
|
+
# @param target_level [Integer] Target compression level
|
|
256
|
+
# @param task_description [String] LLM instruction
|
|
257
|
+
# @return [Integer] Number of cards promoted
|
|
258
|
+
def batch_llm_promote(cards, target_level, task_description)
|
|
259
|
+
return cards.each { |c| promote_card(c) }.size unless @chat
|
|
260
|
+
|
|
261
|
+
count = 0
|
|
262
|
+
cards.each_slice(BATCH_SIZE) do |batch|
|
|
263
|
+
results = llm_batch_promote(batch, task_description)
|
|
264
|
+
|
|
265
|
+
batch.each_with_index do |card, i|
|
|
266
|
+
rebuilt = results[i + 1] # 1-indexed IDs
|
|
267
|
+
if rebuilt
|
|
268
|
+
card.text = rebuilt
|
|
269
|
+
card.compression_level = target_level
|
|
270
|
+
card.updated_at = Time.now
|
|
271
|
+
@adapter.write_card(card)
|
|
272
|
+
else
|
|
273
|
+
# Missing from batch response — fall back to individual
|
|
274
|
+
promote_card(card)
|
|
275
|
+
end
|
|
276
|
+
count += 1
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
count
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Make a batched LLM promotion call with XML-tagged cards and neighbor context
|
|
283
|
+
#
|
|
284
|
+
# @param cards [Array<Card>] Cards to promote
|
|
285
|
+
# @param task_description [String] LLM instruction
|
|
286
|
+
# @return [Hash<Integer, String>] Mapping of card ID (1-indexed) to content
|
|
287
|
+
def llm_batch_promote(cards, task_description)
|
|
288
|
+
@chat.reset_messages!
|
|
289
|
+
|
|
290
|
+
input_cards = cards.each_with_index.map do |card, i|
|
|
291
|
+
neighbor_context = gather_neighbor_context(card)
|
|
292
|
+
context_section = neighbor_context.empty? ? "" : "\nRelated context:\n#{neighbor_context}"
|
|
293
|
+
"<card id=\"#{i + 1}\">\n#{card.text}\nType: #{card.type}\nEntities: #{card.entities.join(", ")}#{context_section}\n</card>"
|
|
294
|
+
end.join("\n\n")
|
|
295
|
+
|
|
296
|
+
prompt = <<~PROMPT
|
|
297
|
+
#{task_description}
|
|
298
|
+
|
|
299
|
+
#{input_cards}
|
|
300
|
+
|
|
301
|
+
Return each result inside <card id="N">RESULT</card> tags matching the input IDs.
|
|
302
|
+
PROMPT
|
|
303
|
+
|
|
304
|
+
response = @chat.ask(prompt)
|
|
305
|
+
parse_card_responses(response.content)
|
|
306
|
+
rescue StandardError => e
|
|
307
|
+
EventStream.emit(type: "memory_promotion_batch_error", error: "#{e.class}: #{e.message}")
|
|
308
|
+
{} # Return empty hash to trigger individual fallback
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# L0 → L1: Raw text → faithful summary
|
|
312
|
+
#
|
|
313
|
+
# @param card [Card] L0 card
|
|
314
|
+
# @return [Card] Updated card at L1
|
|
315
|
+
def compress_l0_to_l1(card)
|
|
316
|
+
if @chat
|
|
317
|
+
summary = llm_summarize(card.text, "Summarize this faithfully in 1-2 sentences:")
|
|
318
|
+
card.text = summary
|
|
319
|
+
else
|
|
320
|
+
EventStream.emit(
|
|
321
|
+
type: "memory_compression_no_llm",
|
|
322
|
+
card_id: card.id,
|
|
323
|
+
from_level: 0,
|
|
324
|
+
to_level: 1,
|
|
325
|
+
)
|
|
326
|
+
end
|
|
327
|
+
card.compression_level = 1
|
|
328
|
+
card.updated_at = Time.now
|
|
329
|
+
@adapter.write_card(card)
|
|
330
|
+
card
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# L1 → L2: Summary → sparse bullets
|
|
334
|
+
#
|
|
335
|
+
# @param card [Card] L1 card
|
|
336
|
+
# @return [Card] Updated card at L2
|
|
337
|
+
def compress_l1_to_l2(card)
|
|
338
|
+
if @chat
|
|
339
|
+
bullets = llm_summarize(card.text, "Convert to 2-3 bullet points:")
|
|
340
|
+
card.text = bullets
|
|
341
|
+
else
|
|
342
|
+
EventStream.emit(
|
|
343
|
+
type: "memory_compression_no_llm",
|
|
344
|
+
card_id: card.id,
|
|
345
|
+
from_level: 1,
|
|
346
|
+
to_level: 2,
|
|
347
|
+
)
|
|
348
|
+
end
|
|
349
|
+
card.compression_level = 2
|
|
350
|
+
card.updated_at = Time.now
|
|
351
|
+
@adapter.write_card(card)
|
|
352
|
+
card
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# L2 → L3: Bullets → schema only (no LLM needed)
|
|
356
|
+
#
|
|
357
|
+
# @param card [Card] L2 card
|
|
358
|
+
# @return [Card] Updated card at L3
|
|
359
|
+
def compress_l2_to_l3(card)
|
|
360
|
+
schema = "Type: #{card.type} | Entities: #{card.entities.join(", ")}"
|
|
361
|
+
card.text = schema
|
|
362
|
+
card.compression_level = 3
|
|
363
|
+
card.updated_at = Time.now
|
|
364
|
+
@adapter.write_card(card)
|
|
365
|
+
card
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# L3 → L4: Schema → embedding + title only
|
|
369
|
+
#
|
|
370
|
+
# @param card [Card] L3 card
|
|
371
|
+
# @return [Card] Updated card at L4
|
|
372
|
+
def compress_l3_to_l4(card)
|
|
373
|
+
card.text = "[Compressed] #{card.type}: #{card.entities.first || "unknown"}"
|
|
374
|
+
card.compression_level = 4
|
|
375
|
+
card.updated_at = Time.now
|
|
376
|
+
@adapter.write_card(card)
|
|
377
|
+
card
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# L4 → L3: Rebuild schema from card metadata
|
|
381
|
+
#
|
|
382
|
+
# @param card [Card] L4 card
|
|
383
|
+
# @return [Card] Updated card at L3
|
|
384
|
+
def promote_l4_to_l3(card)
|
|
385
|
+
card.text = "Type: #{card.type} | Entities: #{card.entities.join(", ")}"
|
|
386
|
+
card.compression_level = 3
|
|
387
|
+
card.updated_at = Time.now
|
|
388
|
+
@adapter.write_card(card)
|
|
389
|
+
card
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# L3 → L2: Rebuild bullets from neighbor context
|
|
393
|
+
#
|
|
394
|
+
# @param card [Card] L3 card
|
|
395
|
+
# @return [Card] Updated card at L2
|
|
396
|
+
def promote_l3_to_l2(card)
|
|
397
|
+
neighbor_context = gather_neighbor_context(card)
|
|
398
|
+
if @chat && !neighbor_context.empty?
|
|
399
|
+
rebuilt = llm_rebuild(card, neighbor_context, "Convert to 2-3 bullet points capturing key details:")
|
|
400
|
+
card.text = rebuilt
|
|
401
|
+
end
|
|
402
|
+
card.compression_level = 2
|
|
403
|
+
card.updated_at = Time.now
|
|
404
|
+
@adapter.write_card(card)
|
|
405
|
+
card
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# L2 → L1: Rebuild faithful summary from neighbor context
|
|
409
|
+
#
|
|
410
|
+
# @param card [Card] L2 card
|
|
411
|
+
# @return [Card] Updated card at L1
|
|
412
|
+
def promote_l2_to_l1(card)
|
|
413
|
+
neighbor_context = gather_neighbor_context(card)
|
|
414
|
+
if @chat && !neighbor_context.empty?
|
|
415
|
+
rebuilt = llm_rebuild(card, neighbor_context, "Write a faithful 1-2 sentence summary:")
|
|
416
|
+
card.text = rebuilt
|
|
417
|
+
end
|
|
418
|
+
card.compression_level = 1
|
|
419
|
+
card.updated_at = Time.now
|
|
420
|
+
@adapter.write_card(card)
|
|
421
|
+
card
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# L1 → L0: Rebuild richer text from neighbor context
|
|
425
|
+
#
|
|
426
|
+
# @param card [Card] L1 card
|
|
427
|
+
# @return [Card] Updated card at L0
|
|
428
|
+
def promote_l1_to_l0(card)
|
|
429
|
+
neighbor_context = gather_neighbor_context(card)
|
|
430
|
+
if @chat && !neighbor_context.empty?
|
|
431
|
+
rebuilt = llm_rebuild(card, neighbor_context, "Expand into a detailed paragraph (max 250 words):")
|
|
432
|
+
card.text = rebuilt
|
|
433
|
+
end
|
|
434
|
+
card.compression_level = 0
|
|
435
|
+
card.updated_at = Time.now
|
|
436
|
+
@adapter.write_card(card)
|
|
437
|
+
card
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Gather text from 1-hop graph neighbors for rehydration
|
|
441
|
+
#
|
|
442
|
+
# @param card [Card] Card to find neighbors for
|
|
443
|
+
# @return [String] Concatenated neighbor context
|
|
444
|
+
def gather_neighbor_context(card)
|
|
445
|
+
edges = @adapter.edges_for(card.id)
|
|
446
|
+
neighbor_ids = edges.map { |e| e.from_id == card.id ? e.to_id : e.from_id }.uniq
|
|
447
|
+
neighbors = neighbor_ids.take(4).filter_map { |nid| @adapter.read_card(nid) }
|
|
448
|
+
neighbors.map(&:text).join("\n")
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# Use LLM to rebuild richer text from card + neighbor context
|
|
452
|
+
#
|
|
453
|
+
# @param card [Card] Card being promoted
|
|
454
|
+
# @param neighbor_context [String] Context from neighbors
|
|
455
|
+
# @param instruction [String] Rebuild instruction
|
|
456
|
+
# @return [String] Rebuilt text
|
|
457
|
+
def llm_rebuild(card, neighbor_context, instruction)
|
|
458
|
+
@chat.reset_messages!
|
|
459
|
+
response = @chat.ask(<<~PROMPT)
|
|
460
|
+
#{instruction}
|
|
461
|
+
|
|
462
|
+
Current card (#{card.type}): #{card.text}
|
|
463
|
+
Entities: #{card.entities.join(", ")}
|
|
464
|
+
|
|
465
|
+
Related context from neighboring cards:
|
|
466
|
+
#{neighbor_context}
|
|
467
|
+
PROMPT
|
|
468
|
+
response.content.strip
|
|
469
|
+
rescue StandardError => e
|
|
470
|
+
EventStream.emit(type: "memory_promotion_llm_error", error: "#{e.class}: #{e.message}")
|
|
471
|
+
card.text # Fall back to current text
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Use LLM to summarize text
|
|
475
|
+
#
|
|
476
|
+
# Resets the background chat before each call to prevent message
|
|
477
|
+
# accumulation across multiple compression operations.
|
|
478
|
+
#
|
|
479
|
+
# @param text [String] Text to summarize
|
|
480
|
+
# @param instruction [String] Summarization instruction
|
|
481
|
+
# @return [String] Summary
|
|
482
|
+
def llm_summarize(text, instruction)
|
|
483
|
+
@chat.reset_messages!
|
|
484
|
+
response = @chat.ask("#{instruction}\n\n#{text}")
|
|
485
|
+
response.content.strip
|
|
486
|
+
rescue StandardError => e
|
|
487
|
+
EventStream.emit(
|
|
488
|
+
type: "memory_compression_llm_error",
|
|
489
|
+
error: "#{e.class}: #{e.message}",
|
|
490
|
+
)
|
|
491
|
+
text # Fall back to original text on LLM error
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|