claude_swarm 1.0.0 → 1.0.2
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/.claude/commands/release.md +1 -1
- data/.claude/hooks/lint-code-files.rb +65 -0
- data/.rubocop.yml +22 -2
- data/CHANGELOG.md +21 -1
- data/CLAUDE.md +1 -1
- data/CONTRIBUTING.md +69 -0
- data/README.md +27 -2
- data/Rakefile +71 -3
- data/analyze_coverage.rb +94 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
- data/docs/v2/README.md +308 -0
- data/docs/v2/guides/claude-code-agents.md +262 -0
- data/docs/v2/guides/complete-tutorial.md +3088 -0
- data/docs/v2/guides/getting-started.md +1456 -0
- data/docs/v2/guides/memory-adapters.md +998 -0
- data/docs/v2/guides/plugins.md +816 -0
- data/docs/v2/guides/quick-start-cli.md +1745 -0
- data/docs/v2/guides/rails-integration.md +1902 -0
- data/docs/v2/guides/swarm-memory.md +599 -0
- data/docs/v2/reference/cli.md +729 -0
- data/docs/v2/reference/ruby-dsl.md +2154 -0
- data/docs/v2/reference/yaml.md +1835 -0
- data/docs-team-swarm.yml +2222 -0
- data/examples/learning-assistant/assistant.md +7 -0
- data/examples/learning-assistant/example-memories/concept-example.md +90 -0
- data/examples/learning-assistant/example-memories/experience-example.md +66 -0
- data/examples/learning-assistant/example-memories/fact-example.md +76 -0
- data/examples/learning-assistant/example-memories/memory-index.md +78 -0
- data/examples/learning-assistant/example-memories/skill-example.md +168 -0
- data/examples/learning-assistant/learning_assistant.rb +34 -0
- data/examples/learning-assistant/learning_assistant.yml +20 -0
- data/examples/v2/dsl/01_basic.rb +44 -0
- data/examples/v2/dsl/02_core_parameters.rb +59 -0
- data/examples/v2/dsl/03_capabilities.rb +71 -0
- data/examples/v2/dsl/04_llm_parameters.rb +56 -0
- data/examples/v2/dsl/05_advanced_flags.rb +73 -0
- data/examples/v2/dsl/06_permissions.rb +80 -0
- data/examples/v2/dsl/07_mcp_server.rb +62 -0
- data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
- data/examples/v2/dsl/09_agent_hooks.rb +67 -0
- data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
- data/examples/v2/dsl/11_delegation.rb +60 -0
- data/examples/v2/dsl/12_complete_integration.rb +137 -0
- data/examples/v2/file_tools_swarm.yml +102 -0
- data/examples/v2/hooks/01_basic_hooks.rb +133 -0
- data/examples/v2/hooks/02_usage_tracking.rb +201 -0
- data/examples/v2/hooks/03_production_monitoring.rb +429 -0
- data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
- data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
- data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
- data/examples/v2/hooks/swarm_summary.sh +44 -0
- data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
- data/examples/v2/hooks/validate_bash.rb +59 -0
- data/examples/v2/multi_directory_permissions.yml +221 -0
- data/examples/v2/node_context_demo.rb +127 -0
- data/examples/v2/node_workflow.rb +173 -0
- data/examples/v2/path_resolution_demo.rb +216 -0
- data/examples/v2/simple-swarm-v2.rb +90 -0
- data/examples/v2/simple-swarm-v2.yml +62 -0
- data/examples/v2/swarm.yml +71 -0
- data/examples/v2/swarm_with_hooks.yml +61 -0
- data/examples/v2/swarm_with_hooks_simple.yml +25 -0
- data/examples/v2/think_tool_demo.rb +62 -0
- data/exe/swarm +6 -0
- data/lib/claude_swarm/claude_mcp_server.rb +0 -6
- data/lib/claude_swarm/cli.rb +10 -3
- data/lib/claude_swarm/commands/ps.rb +19 -20
- data/lib/claude_swarm/commands/show.rb +1 -1
- data/lib/claude_swarm/configuration.rb +10 -12
- data/lib/claude_swarm/mcp_generator.rb +10 -1
- data/lib/claude_swarm/orchestrator.rb +73 -49
- data/lib/claude_swarm/system_utils.rb +37 -11
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm/worktree_manager.rb +1 -0
- data/lib/claude_swarm/yaml_loader.rb +22 -0
- data/lib/claude_swarm.rb +7 -3
- data/lib/swarm_cli/cli.rb +201 -0
- data/lib/swarm_cli/command_registry.rb +61 -0
- data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
- data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
- data/lib/swarm_cli/commands/migrate.rb +55 -0
- data/lib/swarm_cli/commands/run.rb +173 -0
- data/lib/swarm_cli/config_loader.rb +97 -0
- data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
- data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
- data/lib/swarm_cli/interactive_repl.rb +918 -0
- data/lib/swarm_cli/mcp_serve_options.rb +44 -0
- data/lib/swarm_cli/mcp_tools_options.rb +59 -0
- data/lib/swarm_cli/migrate_options.rb +54 -0
- data/lib/swarm_cli/migrator.rb +132 -0
- data/lib/swarm_cli/options.rb +151 -0
- data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
- data/lib/swarm_cli/ui/components/content_block.rb +120 -0
- data/lib/swarm_cli/ui/components/divider.rb +57 -0
- data/lib/swarm_cli/ui/components/panel.rb +62 -0
- data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
- data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
- data/lib/swarm_cli/ui/formatters/number.rb +58 -0
- data/lib/swarm_cli/ui/formatters/text.rb +77 -0
- data/lib/swarm_cli/ui/formatters/time.rb +73 -0
- data/lib/swarm_cli/ui/icons.rb +59 -0
- data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
- data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
- data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
- data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
- data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
- data/lib/swarm_cli/version.rb +5 -0
- data/lib/swarm_cli.rb +44 -0
- data/lib/swarm_memory/adapters/base.rb +141 -0
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
- data/lib/swarm_memory/chat_extension.rb +34 -0
- data/lib/swarm_memory/cli/commands.rb +306 -0
- data/lib/swarm_memory/core/entry.rb +37 -0
- data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
- data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
- data/lib/swarm_memory/core/path_normalizer.rb +75 -0
- data/lib/swarm_memory/core/semantic_index.rb +244 -0
- data/lib/swarm_memory/core/storage.rb +288 -0
- data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
- data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
- data/lib/swarm_memory/dsl/memory_config.rb +113 -0
- data/lib/swarm_memory/embeddings/embedder.rb +36 -0
- data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
- data/lib/swarm_memory/errors.rb +21 -0
- data/lib/swarm_memory/integration/cli_registration.rb +30 -0
- data/lib/swarm_memory/integration/configuration.rb +43 -0
- data/lib/swarm_memory/integration/registration.rb +31 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
- data/lib/swarm_memory/optimization/analyzer.rb +244 -0
- data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
- data/lib/swarm_memory/prompts/memory.md.erb +109 -0
- data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
- data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
- data/lib/swarm_memory/search/semantic_search.rb +112 -0
- data/lib/swarm_memory/search/text_search.rb +42 -0
- data/lib/swarm_memory/search/text_similarity.rb +80 -0
- data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
- data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
- data/lib/swarm_memory/tools/load_skill.rb +313 -0
- data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
- data/lib/swarm_memory/tools/memory_delete.rb +99 -0
- data/lib/swarm_memory/tools/memory_edit.rb +185 -0
- data/lib/swarm_memory/tools/memory_glob.rb +160 -0
- data/lib/swarm_memory/tools/memory_grep.rb +247 -0
- data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
- data/lib/swarm_memory/tools/memory_read.rb +123 -0
- data/lib/swarm_memory/tools/memory_write.rb +231 -0
- data/lib/swarm_memory/utils.rb +50 -0
- data/lib/swarm_memory/version.rb +5 -0
- data/lib/swarm_memory.rb +166 -0
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
- data/lib/swarm_sdk/agent/builder.rb +461 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
- data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
- data/lib/swarm_sdk/agent/chat.rb +1159 -0
- data/lib/swarm_sdk/agent/context.rb +112 -0
- data/lib/swarm_sdk/agent/context_manager.rb +309 -0
- data/lib/swarm_sdk/agent/definition.rb +556 -0
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
- data/lib/swarm_sdk/configuration.rb +296 -0
- data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
- data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
- data/lib/swarm_sdk/context_compactor.rb +340 -0
- data/lib/swarm_sdk/hooks/adapter.rb +359 -0
- data/lib/swarm_sdk/hooks/context.rb +197 -0
- data/lib/swarm_sdk/hooks/definition.rb +80 -0
- data/lib/swarm_sdk/hooks/error.rb +29 -0
- data/lib/swarm_sdk/hooks/executor.rb +146 -0
- data/lib/swarm_sdk/hooks/registry.rb +147 -0
- data/lib/swarm_sdk/hooks/result.rb +150 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
- data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
- data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
- data/lib/swarm_sdk/log_collector.rb +51 -0
- data/lib/swarm_sdk/log_stream.rb +69 -0
- data/lib/swarm_sdk/markdown_parser.rb +75 -0
- data/lib/swarm_sdk/model_aliases.json +5 -0
- data/lib/swarm_sdk/models.json +1 -0
- data/lib/swarm_sdk/models.rb +120 -0
- data/lib/swarm_sdk/node/agent_config.rb +49 -0
- data/lib/swarm_sdk/node/builder.rb +439 -0
- data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
- data/lib/swarm_sdk/node_context.rb +170 -0
- data/lib/swarm_sdk/node_orchestrator.rb +384 -0
- data/lib/swarm_sdk/permissions/config.rb +239 -0
- data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
- data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
- data/lib/swarm_sdk/permissions/validator.rb +173 -0
- data/lib/swarm_sdk/permissions_builder.rb +122 -0
- data/lib/swarm_sdk/plugin.rb +147 -0
- data/lib/swarm_sdk/plugin_registry.rb +101 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
- data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
- data/lib/swarm_sdk/result.rb +97 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
- data/lib/swarm_sdk/swarm/builder.rb +586 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
- data/lib/swarm_sdk/swarm.rb +982 -0
- data/lib/swarm_sdk/tools/bash.rb +274 -0
- data/lib/swarm_sdk/tools/clock.rb +44 -0
- data/lib/swarm_sdk/tools/delegate.rb +164 -0
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
- data/lib/swarm_sdk/tools/edit.rb +150 -0
- data/lib/swarm_sdk/tools/glob.rb +158 -0
- data/lib/swarm_sdk/tools/grep.rb +228 -0
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
- data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
- data/lib/swarm_sdk/tools/read.rb +251 -0
- data/lib/swarm_sdk/tools/registry.rb +93 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
- data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
- data/lib/swarm_sdk/tools/think.rb +95 -0
- data/lib/swarm_sdk/tools/todo_write.rb +216 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
- data/lib/swarm_sdk/tools/write.rb +117 -0
- data/lib/swarm_sdk/utils.rb +50 -0
- data/lib/swarm_sdk/version.rb +5 -0
- data/lib/swarm_sdk.rb +157 -0
- data/llm.v2.txt +13407 -0
- data/rubocop/cop/security/no_reflection_methods.rb +47 -0
- data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
- data/swarm_cli.gemspec +57 -0
- data/swarm_memory.gemspec +28 -0
- data/swarm_sdk.gemspec +41 -0
- data/team.yml +1 -1
- data/team_full.yml +1875 -0
- data/{team_v2.yml → team_sdk.yml} +121 -52
- metadata +247 -4
- data/EXAMPLES.md +0 -164
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmMemory
|
|
4
|
+
module Core
|
|
5
|
+
# Semantic search abstraction layer
|
|
6
|
+
#
|
|
7
|
+
# Provides embedding computation and semantic search operations
|
|
8
|
+
# that work with any storage adapter. Easily replaceable with
|
|
9
|
+
# vector database implementations (Qdrant, Milvus, Chroma, etc.)
|
|
10
|
+
#
|
|
11
|
+
# Uses hybrid search: combines semantic similarity with keyword matching
|
|
12
|
+
# for better recall accuracy.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# index = SemanticIndex.new(adapter: adapter, embedder: embedder)
|
|
16
|
+
# results = index.search(query: "how to debug", top_k: 5, threshold: 0.7)
|
|
17
|
+
class SemanticIndex
|
|
18
|
+
# Default weights for hybrid scoring (optimal: 50/50 discovered via systematic evaluation)
|
|
19
|
+
# Configurable via ENV vars: SWARM_MEMORY_SEMANTIC_WEIGHT, SWARM_MEMORY_KEYWORD_WEIGHT
|
|
20
|
+
DEFAULT_SEMANTIC_WEIGHT = (ENV["SWARM_MEMORY_SEMANTIC_WEIGHT"] || "0.5").to_f
|
|
21
|
+
DEFAULT_KEYWORD_WEIGHT = (ENV["SWARM_MEMORY_KEYWORD_WEIGHT"] || "0.5").to_f
|
|
22
|
+
|
|
23
|
+
# @param adapter [Adapters::Base] Storage adapter
|
|
24
|
+
# @param embedder [Embeddings::Embedder] Embedding model
|
|
25
|
+
# @param semantic_weight [Float] Weight for semantic similarity (0.0-1.0)
|
|
26
|
+
# @param keyword_weight [Float] Weight for keyword matching (0.0-1.0)
|
|
27
|
+
def initialize(adapter:, embedder:, semantic_weight: DEFAULT_SEMANTIC_WEIGHT, keyword_weight: DEFAULT_KEYWORD_WEIGHT)
|
|
28
|
+
@adapter = adapter
|
|
29
|
+
@embedder = embedder
|
|
30
|
+
@semantic_weight = semantic_weight
|
|
31
|
+
@keyword_weight = keyword_weight
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Compute embedding for text
|
|
35
|
+
#
|
|
36
|
+
# @param text [String] Text to embed
|
|
37
|
+
# @return [Array<Float>] Embedding vector
|
|
38
|
+
def compute_embedding(text)
|
|
39
|
+
@embedder.embed(text)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Semantic search by text query
|
|
43
|
+
#
|
|
44
|
+
# @param query [String] Search query
|
|
45
|
+
# @param top_k [Integer] Number of results to return
|
|
46
|
+
# @param threshold [Float] Minimum similarity score (0.0-1.0)
|
|
47
|
+
# @param filter [Hash, nil] Optional metadata filters (e.g., { "type" => "skill" })
|
|
48
|
+
# @return [Array<Hash>] Results with similarity scores, sorted by similarity descending
|
|
49
|
+
#
|
|
50
|
+
# @example
|
|
51
|
+
# results = index.search(
|
|
52
|
+
# query: "how to create a swarm",
|
|
53
|
+
# top_k: 3,
|
|
54
|
+
# threshold: 0.65,
|
|
55
|
+
# filter: { "type" => "skill" }
|
|
56
|
+
# )
|
|
57
|
+
#
|
|
58
|
+
# results.each do |result|
|
|
59
|
+
# puts "#{result[:path]} (#{result[:similarity]})"
|
|
60
|
+
# puts result[:title]
|
|
61
|
+
# end
|
|
62
|
+
def search(query:, top_k: 10, threshold: 0.0, filter: nil)
|
|
63
|
+
# Extract keywords from query for keyword matching
|
|
64
|
+
query_keywords = extract_keywords(query)
|
|
65
|
+
|
|
66
|
+
# Compute query embedding
|
|
67
|
+
query_embedding = compute_embedding(query)
|
|
68
|
+
|
|
69
|
+
# Delegate to adapter-specific search (gets semantic similarity only)
|
|
70
|
+
# Use threshold of 0.0 to get all results, we'll filter after hybrid scoring
|
|
71
|
+
results = @adapter.semantic_search(
|
|
72
|
+
embedding: query_embedding,
|
|
73
|
+
top_k: top_k * 3, # Get extra for reranking
|
|
74
|
+
threshold: 0.0, # No threshold yet - will apply after hybrid scoring
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Calculate hybrid scores (semantic + keyword)
|
|
78
|
+
results = calculate_hybrid_scores(results, query_keywords)
|
|
79
|
+
|
|
80
|
+
# Apply metadata filters if provided
|
|
81
|
+
results = apply_filters(results, filter) if filter
|
|
82
|
+
|
|
83
|
+
# Filter by threshold on hybrid score
|
|
84
|
+
results = results.select { |r| r[:similarity] >= threshold }
|
|
85
|
+
|
|
86
|
+
# Return top K after filtering and reranking
|
|
87
|
+
results.take(top_k)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Find similar entries by embedding vector
|
|
91
|
+
#
|
|
92
|
+
# @param embedding [Array<Float>] Embedding vector
|
|
93
|
+
# @param top_k [Integer] Number of results to return
|
|
94
|
+
# @param threshold [Float] Minimum similarity score (0.0-1.0)
|
|
95
|
+
# @param filter [Hash, nil] Optional metadata filters
|
|
96
|
+
# @return [Array<Hash>] Similar entries sorted by similarity descending
|
|
97
|
+
def find_similar(embedding:, top_k: 10, threshold: 0.0, filter: nil)
|
|
98
|
+
results = @adapter.semantic_search(
|
|
99
|
+
embedding: embedding,
|
|
100
|
+
top_k: top_k * 2,
|
|
101
|
+
threshold: threshold,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Apply metadata filters if provided
|
|
105
|
+
results = apply_filters(results, filter) if filter
|
|
106
|
+
|
|
107
|
+
# Return top K after filtering
|
|
108
|
+
results.take(top_k)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
# Extract keywords from query text
|
|
114
|
+
#
|
|
115
|
+
# Removes common words and punctuation, lowercases everything.
|
|
116
|
+
#
|
|
117
|
+
# @param text [String] Query text
|
|
118
|
+
# @return [Array<String>] Extracted keywords
|
|
119
|
+
def extract_keywords(text)
|
|
120
|
+
# Common stop words to ignore
|
|
121
|
+
stop_words = [
|
|
122
|
+
"a",
|
|
123
|
+
"an",
|
|
124
|
+
"and",
|
|
125
|
+
"are",
|
|
126
|
+
"as",
|
|
127
|
+
"at",
|
|
128
|
+
"be",
|
|
129
|
+
"by",
|
|
130
|
+
"for",
|
|
131
|
+
"from",
|
|
132
|
+
"has",
|
|
133
|
+
"have",
|
|
134
|
+
"in",
|
|
135
|
+
"is",
|
|
136
|
+
"it",
|
|
137
|
+
"of",
|
|
138
|
+
"on",
|
|
139
|
+
"that",
|
|
140
|
+
"the",
|
|
141
|
+
"this",
|
|
142
|
+
"to",
|
|
143
|
+
"was",
|
|
144
|
+
"will",
|
|
145
|
+
"with",
|
|
146
|
+
"how",
|
|
147
|
+
"what",
|
|
148
|
+
"when",
|
|
149
|
+
"where",
|
|
150
|
+
"why",
|
|
151
|
+
"who",
|
|
152
|
+
"which",
|
|
153
|
+
"do",
|
|
154
|
+
"does",
|
|
155
|
+
"did",
|
|
156
|
+
"can",
|
|
157
|
+
"could",
|
|
158
|
+
"should",
|
|
159
|
+
"would",
|
|
160
|
+
"may",
|
|
161
|
+
"might",
|
|
162
|
+
"must",
|
|
163
|
+
"me",
|
|
164
|
+
"my",
|
|
165
|
+
"you",
|
|
166
|
+
"your",
|
|
167
|
+
"we",
|
|
168
|
+
"us",
|
|
169
|
+
"our",
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
# Extract words (lowercase, alphanumeric only)
|
|
173
|
+
words = text.downcase
|
|
174
|
+
.gsub(/[^a-z0-9\s\-]/, " ") # Remove punctuation except hyphens
|
|
175
|
+
.split(/\s+/)
|
|
176
|
+
.reject { |w| w.length < 2 } # Skip single chars
|
|
177
|
+
.reject { |w| stop_words.include?(w) } # Skip stop words
|
|
178
|
+
|
|
179
|
+
words.uniq
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Calculate hybrid scores combining semantic similarity and keyword matching
|
|
183
|
+
#
|
|
184
|
+
# @param results [Array<Hash>] Results with semantic :similarity scores
|
|
185
|
+
# @param query_keywords [Array<String>] Keywords from query
|
|
186
|
+
# @return [Array<Hash>] Results with updated :similarity (hybrid score) and debug info
|
|
187
|
+
def calculate_hybrid_scores(results, query_keywords)
|
|
188
|
+
results.map do |result|
|
|
189
|
+
semantic_score = result[:similarity]
|
|
190
|
+
keyword_score = calculate_keyword_score(result, query_keywords)
|
|
191
|
+
|
|
192
|
+
# Hybrid score: weighted combination
|
|
193
|
+
hybrid_score = (@semantic_weight * semantic_score) + (@keyword_weight * keyword_score)
|
|
194
|
+
|
|
195
|
+
# Update result with hybrid score and debug info
|
|
196
|
+
result.merge(
|
|
197
|
+
similarity: hybrid_score,
|
|
198
|
+
semantic_score: semantic_score,
|
|
199
|
+
keyword_score: keyword_score,
|
|
200
|
+
)
|
|
201
|
+
end.sort_by { |r| -r[:similarity] }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Calculate keyword matching score based on tag overlap
|
|
205
|
+
#
|
|
206
|
+
# @param result [Hash] Search result with :metadata containing tags
|
|
207
|
+
# @param query_keywords [Array<String>] Keywords from query
|
|
208
|
+
# @return [Float] Keyword score (0.0-1.0)
|
|
209
|
+
def calculate_keyword_score(result, query_keywords)
|
|
210
|
+
return 0.0 if query_keywords.empty?
|
|
211
|
+
|
|
212
|
+
# Get tags from metadata
|
|
213
|
+
tags = result.dig(:metadata, "tags") || result.dig(:metadata, :tags) || []
|
|
214
|
+
return 0.0 if tags.empty?
|
|
215
|
+
|
|
216
|
+
# Normalize tags to lowercase
|
|
217
|
+
normalized_tags = tags.map(&:downcase)
|
|
218
|
+
|
|
219
|
+
# Count keyword matches (fuzzy matching - substring or contains)
|
|
220
|
+
matches = query_keywords.count do |keyword|
|
|
221
|
+
normalized_tags.any? { |tag| tag.include?(keyword) || keyword.include?(tag) }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Normalize to 0-1 scale
|
|
225
|
+
# Use min(query_keywords.size, 5) as denominator to avoid penalizing long queries
|
|
226
|
+
denominator = [query_keywords.size, 5].min
|
|
227
|
+
matches.to_f / denominator
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Apply metadata filters to results
|
|
231
|
+
#
|
|
232
|
+
# @param results [Array<Hash>] Search results
|
|
233
|
+
# @param filter [Hash] Metadata filters
|
|
234
|
+
# @return [Array<Hash>] Filtered results
|
|
235
|
+
def apply_filters(results, filter)
|
|
236
|
+
results.select do |result|
|
|
237
|
+
filter.all? do |key, value|
|
|
238
|
+
result.dig(:metadata, key) == value || result.dig(:metadata, key.to_s) == value
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmMemory
|
|
4
|
+
module Core
|
|
5
|
+
# High-level storage orchestration
|
|
6
|
+
#
|
|
7
|
+
# Coordinates adapter operations, path normalization, embedding generation,
|
|
8
|
+
# and metadata extraction.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# adapter = Adapters::FilesystemAdapter.new(persist_to: ".swarm/memory.json")
|
|
12
|
+
# storage = Storage.new(adapter: adapter)
|
|
13
|
+
# storage.write(file_path: "concepts/ruby", content: "...", title: "Ruby Classes")
|
|
14
|
+
class Storage
|
|
15
|
+
attr_reader :adapter
|
|
16
|
+
|
|
17
|
+
# Initialize storage with an adapter
|
|
18
|
+
#
|
|
19
|
+
# @param adapter [Adapters::Base] Storage adapter
|
|
20
|
+
# @param embedder [Embeddings::Embedder, nil] Optional embedder for semantic search
|
|
21
|
+
def initialize(adapter:, embedder: nil)
|
|
22
|
+
raise ArgumentError, "adapter is required" unless adapter.is_a?(Adapters::Base)
|
|
23
|
+
|
|
24
|
+
@adapter = adapter
|
|
25
|
+
@embedder = embedder
|
|
26
|
+
|
|
27
|
+
# Create semantic index if embedder is provided
|
|
28
|
+
@semantic_index = if embedder
|
|
29
|
+
SemanticIndex.new(adapter: adapter, embedder: embedder)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Get semantic index for semantic search operations
|
|
34
|
+
#
|
|
35
|
+
# @return [SemanticIndex, nil] Semantic index instance or nil if no embedder
|
|
36
|
+
attr_reader :semantic_index
|
|
37
|
+
|
|
38
|
+
# Write content to storage
|
|
39
|
+
#
|
|
40
|
+
# @param file_path [String] Path to store content (with .md extension)
|
|
41
|
+
# @param content [String] Content to store (pure markdown)
|
|
42
|
+
# @param title [String] Brief title
|
|
43
|
+
# @param metadata [Hash, nil] Optional metadata
|
|
44
|
+
# @param generate_embedding [Boolean] Whether to generate embedding (default: true if embedder present)
|
|
45
|
+
# @return [Entry] The created entry
|
|
46
|
+
def write(file_path:, content:, title:, metadata: nil, generate_embedding: nil)
|
|
47
|
+
# Normalize path
|
|
48
|
+
normalized_path = PathNormalizer.normalize(file_path)
|
|
49
|
+
|
|
50
|
+
# Generate embedding if requested and embedder available
|
|
51
|
+
embedding = nil
|
|
52
|
+
should_embed = generate_embedding.nil? ? !@embedder.nil? : generate_embedding
|
|
53
|
+
|
|
54
|
+
if should_embed && @embedder
|
|
55
|
+
begin
|
|
56
|
+
# Build searchable text for better semantic matching
|
|
57
|
+
# Uses title + tags + first paragraph instead of full content
|
|
58
|
+
searchable_text = build_searchable_text(content, title, metadata)
|
|
59
|
+
|
|
60
|
+
# ALWAYS emit to LogStream (create if needed for debugging)
|
|
61
|
+
# This ensures we can see what's being embedded
|
|
62
|
+
begin
|
|
63
|
+
if defined?(SwarmSDK::LogStream)
|
|
64
|
+
SwarmSDK::LogStream.emit(
|
|
65
|
+
type: "memory_embedding_generated",
|
|
66
|
+
file_path: normalized_path,
|
|
67
|
+
title: title,
|
|
68
|
+
searchable_text_length: searchable_text.length,
|
|
69
|
+
searchable_text_preview: searchable_text.slice(0, 300),
|
|
70
|
+
full_searchable_text: searchable_text,
|
|
71
|
+
metadata_tags: metadata&.dig("tags"),
|
|
72
|
+
metadata_domain: metadata&.dig("domain"),
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
# Don't fail if logging fails
|
|
77
|
+
warn("Failed to log embedding: #{e.message}")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
embedding = @embedder.embed(searchable_text)
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
# Don't fail write if embedding generation fails
|
|
83
|
+
warn("Warning: Failed to generate embedding for #{normalized_path}: #{e.message}")
|
|
84
|
+
embedding = nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Write to adapter (metadata passed from tool parameters)
|
|
89
|
+
@adapter.write(
|
|
90
|
+
file_path: normalized_path,
|
|
91
|
+
content: content,
|
|
92
|
+
title: title,
|
|
93
|
+
embedding: embedding,
|
|
94
|
+
metadata: metadata,
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Read content from storage
|
|
99
|
+
#
|
|
100
|
+
# @param file_path [String] Path to read from
|
|
101
|
+
# @return [String] Content at the path
|
|
102
|
+
def read(file_path:)
|
|
103
|
+
normalized_path = PathNormalizer.normalize(file_path)
|
|
104
|
+
@adapter.read(file_path: normalized_path)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Read full entry with metadata
|
|
108
|
+
#
|
|
109
|
+
# @param file_path [String] Path to read from
|
|
110
|
+
# @return [Entry] Full entry object
|
|
111
|
+
def read_entry(file_path:)
|
|
112
|
+
normalized_path = PathNormalizer.normalize(file_path)
|
|
113
|
+
@adapter.read_entry(file_path: normalized_path)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Delete an entry
|
|
117
|
+
#
|
|
118
|
+
# @param file_path [String] Path to delete
|
|
119
|
+
# @return [void]
|
|
120
|
+
def delete(file_path:)
|
|
121
|
+
normalized_path = PathNormalizer.normalize(file_path)
|
|
122
|
+
@adapter.delete(file_path: normalized_path)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# List all entries
|
|
126
|
+
#
|
|
127
|
+
# @param prefix [String, nil] Optional prefix filter
|
|
128
|
+
# @return [Array<Hash>] Entry metadata
|
|
129
|
+
def list(prefix: nil)
|
|
130
|
+
@adapter.list(prefix: prefix)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Search by glob pattern
|
|
134
|
+
#
|
|
135
|
+
# @param pattern [String] Glob pattern
|
|
136
|
+
# @return [Array<Hash>] Matching entries
|
|
137
|
+
def glob(pattern:)
|
|
138
|
+
@adapter.glob(pattern: pattern)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Search by content pattern
|
|
142
|
+
#
|
|
143
|
+
# @param pattern [String] Regex pattern
|
|
144
|
+
# @param case_insensitive [Boolean] Case-insensitive search
|
|
145
|
+
# @param output_mode [String] Output mode
|
|
146
|
+
# @param path [String, nil] Optional path prefix filter
|
|
147
|
+
# @return [Array<Hash>] Search results
|
|
148
|
+
def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches", path: nil)
|
|
149
|
+
@adapter.grep(
|
|
150
|
+
pattern: pattern,
|
|
151
|
+
case_insensitive: case_insensitive,
|
|
152
|
+
output_mode: output_mode,
|
|
153
|
+
path: path,
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Clear all entries
|
|
158
|
+
#
|
|
159
|
+
# @return [void]
|
|
160
|
+
def clear
|
|
161
|
+
@adapter.clear
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Get total storage size
|
|
165
|
+
#
|
|
166
|
+
# @return [Integer] Size in bytes
|
|
167
|
+
def total_size
|
|
168
|
+
@adapter.total_size
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Get number of entries
|
|
172
|
+
#
|
|
173
|
+
# @return [Integer] Entry count
|
|
174
|
+
def size
|
|
175
|
+
@adapter.size
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Get all entries (for optimization/analysis)
|
|
179
|
+
#
|
|
180
|
+
# @return [Hash<String, Entry>] All entries
|
|
181
|
+
def all_entries
|
|
182
|
+
@adapter.all_entries
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
# Build searchable text for embedding
|
|
188
|
+
#
|
|
189
|
+
# Creates a condensed representation optimized for semantic search.
|
|
190
|
+
# Uses title + tags + first paragraph instead of full content.
|
|
191
|
+
#
|
|
192
|
+
# @param content [String] Full entry content
|
|
193
|
+
# @param title [String] Entry title
|
|
194
|
+
# @param metadata [Hash, nil] Entry metadata
|
|
195
|
+
# @return [String] Searchable text for embedding
|
|
196
|
+
def build_searchable_text(content, title, metadata)
|
|
197
|
+
parts = []
|
|
198
|
+
|
|
199
|
+
# 1. Title (most important for matching)
|
|
200
|
+
parts << "Title: #{title}"
|
|
201
|
+
|
|
202
|
+
# 2. Tags (critical keywords that users would search for)
|
|
203
|
+
if metadata && metadata["tags"]&.any?
|
|
204
|
+
parts << "Tags: #{metadata["tags"].join(", ")}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# 3. Domain (additional context)
|
|
208
|
+
if metadata && metadata["domain"]
|
|
209
|
+
parts << "Domain: #{metadata["domain"]}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# 4. First paragraph (summary/description)
|
|
213
|
+
first_para = extract_first_paragraph(content)
|
|
214
|
+
parts << "Summary: #{first_para}" if first_para
|
|
215
|
+
|
|
216
|
+
parts.join("\n\n")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Extract first meaningful paragraph from content
|
|
220
|
+
#
|
|
221
|
+
# Skips YAML frontmatter, headings, and empty lines to find
|
|
222
|
+
# the first substantive paragraph.
|
|
223
|
+
#
|
|
224
|
+
# Character limit can be controlled via SWARM_MEMORY_EMBEDDING_MAX_CHARS:
|
|
225
|
+
# - Default: 300
|
|
226
|
+
# - -1: Unlimited (use full content)
|
|
227
|
+
# - Any positive number: Custom limit
|
|
228
|
+
#
|
|
229
|
+
# @param content [String] Full content
|
|
230
|
+
# @return [String, nil] First paragraph (max chars based on env var) or nil
|
|
231
|
+
def extract_first_paragraph(content)
|
|
232
|
+
return if content.nil? || content.strip.empty?
|
|
233
|
+
|
|
234
|
+
# Get character limit from environment variable
|
|
235
|
+
# Default: 300, -1 = unlimited
|
|
236
|
+
max_chars = (ENV["SWARM_MEMORY_EMBEDDING_MAX_CHARS"] || "1200").to_i
|
|
237
|
+
unlimited = max_chars == -1
|
|
238
|
+
|
|
239
|
+
lines = content.lines
|
|
240
|
+
|
|
241
|
+
# Skip YAML frontmatter (--- ... ---)
|
|
242
|
+
in_frontmatter = false
|
|
243
|
+
lines = lines.drop_while do |line|
|
|
244
|
+
if line.strip == "---"
|
|
245
|
+
in_frontmatter = !in_frontmatter
|
|
246
|
+
true
|
|
247
|
+
else
|
|
248
|
+
in_frontmatter
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Find first non-heading, non-empty paragraph(s)
|
|
253
|
+
paragraph = []
|
|
254
|
+
lines.each do |line|
|
|
255
|
+
stripped = line.strip
|
|
256
|
+
|
|
257
|
+
# Skip empty lines
|
|
258
|
+
next if stripped.empty?
|
|
259
|
+
|
|
260
|
+
# Stop if we hit a heading after collecting some text (unless unlimited)
|
|
261
|
+
if stripped.start_with?("#") && paragraph.any? && !unlimited
|
|
262
|
+
break
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Skip headings
|
|
266
|
+
next if stripped.start_with?("#")
|
|
267
|
+
|
|
268
|
+
# Skip code blocks
|
|
269
|
+
next if stripped.start_with?("```")
|
|
270
|
+
|
|
271
|
+
# Add line to paragraph
|
|
272
|
+
paragraph << stripped
|
|
273
|
+
|
|
274
|
+
# Stop if we have enough text (unless unlimited)
|
|
275
|
+
unless unlimited
|
|
276
|
+
break if paragraph.join(" ").length > (max_chars - 100)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
return if paragraph.empty?
|
|
281
|
+
|
|
282
|
+
# Join and cap at max_chars (or don't cap if unlimited)
|
|
283
|
+
text = paragraph.join(" ")
|
|
284
|
+
unlimited ? text : text.slice(0, max_chars)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmMemory
|
|
4
|
+
module Core
|
|
5
|
+
# StorageReadTracker manages read-entry tracking for all agents
|
|
6
|
+
#
|
|
7
|
+
# This module maintains a global registry of which memory entries each agent
|
|
8
|
+
# has read during their conversation. This enables enforcement of the
|
|
9
|
+
# "read-before-edit" rule that ensures agents have context before modifying entries.
|
|
10
|
+
#
|
|
11
|
+
# Each agent maintains an independent set of read entries, keyed by agent identifier.
|
|
12
|
+
module StorageReadTracker
|
|
13
|
+
@read_entries = {}
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Register that an agent has read a storage entry
|
|
18
|
+
#
|
|
19
|
+
# @param agent_id [Symbol] The agent identifier
|
|
20
|
+
# @param entry_path [String] The storage entry path
|
|
21
|
+
# @return [void]
|
|
22
|
+
def register_read(agent_id, entry_path)
|
|
23
|
+
@mutex.synchronize do
|
|
24
|
+
@read_entries[agent_id] ||= Set.new
|
|
25
|
+
@read_entries[agent_id] << entry_path
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check if an agent has read a storage entry
|
|
30
|
+
#
|
|
31
|
+
# @param agent_id [Symbol] The agent identifier
|
|
32
|
+
# @param entry_path [String] The storage entry path
|
|
33
|
+
# @return [Boolean] true if the agent has read this entry
|
|
34
|
+
def entry_read?(agent_id, entry_path)
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
return false unless @read_entries[agent_id]
|
|
37
|
+
|
|
38
|
+
@read_entries[agent_id].include?(entry_path)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Clear read history for an agent (useful for testing)
|
|
43
|
+
#
|
|
44
|
+
# @param agent_id [Symbol] The agent identifier
|
|
45
|
+
# @return [void]
|
|
46
|
+
def clear(agent_id)
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
@read_entries.delete(agent_id)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Clear all read history (useful for testing)
|
|
53
|
+
#
|
|
54
|
+
# @return [void]
|
|
55
|
+
def clear_all
|
|
56
|
+
@mutex.synchronize do
|
|
57
|
+
@read_entries.clear
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmMemory
|
|
4
|
+
module DSL
|
|
5
|
+
# Extension module that injects memory DSL into SwarmSDK::Agent::Builder
|
|
6
|
+
#
|
|
7
|
+
# This module is included into Agent::Builder when swarm_memory is required,
|
|
8
|
+
# adding the `memory` configuration method.
|
|
9
|
+
module BuilderExtension
|
|
10
|
+
# Configure persistent memory for this agent
|
|
11
|
+
#
|
|
12
|
+
# @example Interactive mode (default) - Learn and retrieve
|
|
13
|
+
# memory do
|
|
14
|
+
# directory ".swarm/agent-memory"
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @example Retrieval mode - Read-only Q&A
|
|
18
|
+
# memory do
|
|
19
|
+
# directory "team-knowledge/"
|
|
20
|
+
# mode :retrieval
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example Researcher mode - Knowledge extraction
|
|
24
|
+
# memory do
|
|
25
|
+
# directory "team-knowledge/"
|
|
26
|
+
# mode :researcher
|
|
27
|
+
# end
|
|
28
|
+
def memory(&block)
|
|
29
|
+
@memory_config = SwarmMemory::DSL::MemoryConfig.new
|
|
30
|
+
@memory_config.instance_eval(&block) if block_given?
|
|
31
|
+
@memory_config
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Inject memory DSL into Agent::Builder when this file is loaded
|
|
38
|
+
if defined?(SwarmSDK::Agent::Builder)
|
|
39
|
+
SwarmSDK::Agent::Builder.include(SwarmMemory::DSL::BuilderExtension)
|
|
40
|
+
end
|