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,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
module DocumentConverters
|
|
7
|
+
# DOCX document converter
|
|
8
|
+
#
|
|
9
|
+
# Converts DOCX files to text and extracts images.
|
|
10
|
+
# Requires the docx gem (which includes rubyzip).
|
|
11
|
+
class DocxConverter < Base
|
|
12
|
+
class << self
|
|
13
|
+
# @return [String] gem name
|
|
14
|
+
def gem_name
|
|
15
|
+
"docx"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [String] format name
|
|
19
|
+
def format_name
|
|
20
|
+
"DOCX"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Array<String>] supported extensions
|
|
24
|
+
def extensions
|
|
25
|
+
[".docx"]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Convert DOCX to text with optional image attachments
|
|
30
|
+
#
|
|
31
|
+
# @param file_path [String] path to DOCX file
|
|
32
|
+
# @return [String, RubyLLM::Content] text or content with images
|
|
33
|
+
def convert(file_path)
|
|
34
|
+
return unsupported_format_message unless self.class.available?
|
|
35
|
+
return error("Legacy .doc format not supported") if file_path.end_with?(".doc")
|
|
36
|
+
|
|
37
|
+
require "docx"
|
|
38
|
+
doc = Docx::Document.open(file_path)
|
|
39
|
+
|
|
40
|
+
# Extract text content
|
|
41
|
+
output = build_text_output(doc, file_path)
|
|
42
|
+
|
|
43
|
+
# Extract images (inline - no separate class)
|
|
44
|
+
image_paths = extract_images(file_path)
|
|
45
|
+
|
|
46
|
+
if image_paths.any?
|
|
47
|
+
content = RubyLLM::Content.new(output)
|
|
48
|
+
image_paths.each { |path| content.add_attachment(path) }
|
|
49
|
+
content
|
|
50
|
+
else
|
|
51
|
+
output
|
|
52
|
+
end
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
error("DOCX conversion failed: #{e.message}")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# Build text output from DOCX document
|
|
60
|
+
#
|
|
61
|
+
# @param doc [Docx::Document] opened document
|
|
62
|
+
# @param file_path [String] original file path
|
|
63
|
+
# @return [String] formatted text output
|
|
64
|
+
def build_text_output(doc, file_path)
|
|
65
|
+
output = []
|
|
66
|
+
output << "DOCX: #{File.basename(file_path)}"
|
|
67
|
+
output << "=" * 60
|
|
68
|
+
output << ""
|
|
69
|
+
|
|
70
|
+
# Paragraphs
|
|
71
|
+
doc.paragraphs.each do |para|
|
|
72
|
+
text = para.text.strip
|
|
73
|
+
output << text unless text.empty?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Tables
|
|
77
|
+
doc.tables.each_with_index do |table, idx|
|
|
78
|
+
output << ""
|
|
79
|
+
output << "Table #{idx + 1}:"
|
|
80
|
+
output << "-" * 40
|
|
81
|
+
table.rows.each do |row|
|
|
82
|
+
cells = row.cells.map(&:text)
|
|
83
|
+
output << cells.join(" | ")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
output.join("\n")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Extract images from DOCX ZIP (word/media/)
|
|
91
|
+
#
|
|
92
|
+
# @param docx_path [String] path to DOCX file
|
|
93
|
+
# @return [Array<String>] paths to extracted image files
|
|
94
|
+
def extract_images(docx_path)
|
|
95
|
+
require "zip"
|
|
96
|
+
images = []
|
|
97
|
+
temp_dir = Dir.mktmpdir("docx_#{Process.pid}")
|
|
98
|
+
|
|
99
|
+
Zip::File.open(docx_path) do |zip|
|
|
100
|
+
zip.each do |entry|
|
|
101
|
+
next unless entry.name.start_with?("word/media/")
|
|
102
|
+
|
|
103
|
+
ext = File.extname(entry.name).downcase
|
|
104
|
+
next unless [".png", ".jpg", ".jpeg", ".gif"].include?(ext)
|
|
105
|
+
|
|
106
|
+
path = File.join(temp_dir, File.basename(entry.name))
|
|
107
|
+
entry.extract(path)
|
|
108
|
+
images << path
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
images
|
|
113
|
+
rescue StandardError
|
|
114
|
+
[] # Silently ignore extraction failures
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
module DocumentConverters
|
|
7
|
+
# PDF document converter
|
|
8
|
+
#
|
|
9
|
+
# Converts PDF files to text and extracts JPEG images.
|
|
10
|
+
# Requires the pdf-reader gem.
|
|
11
|
+
class PdfConverter < Base
|
|
12
|
+
class << self
|
|
13
|
+
# @return [String] gem name
|
|
14
|
+
def gem_name
|
|
15
|
+
"pdf-reader"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [String] format name
|
|
19
|
+
def format_name
|
|
20
|
+
"PDF"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Array<String>] supported extensions
|
|
24
|
+
def extensions
|
|
25
|
+
[".pdf"]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Convert PDF to text with optional image attachments
|
|
30
|
+
#
|
|
31
|
+
# @param file_path [String] path to PDF file
|
|
32
|
+
# @return [String, RubyLLM::Content] text or content with images
|
|
33
|
+
def convert(file_path)
|
|
34
|
+
return unsupported_format_message unless self.class.available?
|
|
35
|
+
|
|
36
|
+
require "pdf-reader"
|
|
37
|
+
reader = PDF::Reader.new(file_path)
|
|
38
|
+
|
|
39
|
+
# Extract text from all pages
|
|
40
|
+
output = build_text_output(reader, file_path)
|
|
41
|
+
|
|
42
|
+
# Extract JPEG images (inline - no separate class)
|
|
43
|
+
image_paths = extract_jpeg_images(reader)
|
|
44
|
+
|
|
45
|
+
# Return with images if any extracted
|
|
46
|
+
if image_paths.any?
|
|
47
|
+
content = RubyLLM::Content.new(output)
|
|
48
|
+
image_paths.each { |path| content.add_attachment(path) }
|
|
49
|
+
content
|
|
50
|
+
else
|
|
51
|
+
output
|
|
52
|
+
end
|
|
53
|
+
rescue PDF::Reader::MalformedPDFError => e
|
|
54
|
+
error("Malformed PDF: #{e.message}")
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
error("PDF conversion failed: #{e.message}")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Build text output from PDF pages
|
|
62
|
+
#
|
|
63
|
+
# @param reader [PDF::Reader] initialized reader
|
|
64
|
+
# @param file_path [String] original file path
|
|
65
|
+
# @return [String] formatted text output
|
|
66
|
+
def build_text_output(reader, file_path)
|
|
67
|
+
output = []
|
|
68
|
+
output << "PDF: #{File.basename(file_path)}"
|
|
69
|
+
output << "=" * 60
|
|
70
|
+
output << "Pages: #{reader.page_count}"
|
|
71
|
+
output << ""
|
|
72
|
+
|
|
73
|
+
reader.pages.each_with_index do |page, idx|
|
|
74
|
+
output << "Page #{idx + 1}:"
|
|
75
|
+
output << "-" * 60
|
|
76
|
+
text = page.text.strip
|
|
77
|
+
output << (text.empty? ? "(No text)" : text)
|
|
78
|
+
output << ""
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
output.join("\n")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Extract JPEG images only (LLM API compatible)
|
|
85
|
+
#
|
|
86
|
+
# @param reader [PDF::Reader] initialized reader
|
|
87
|
+
# @return [Array<String>] paths to extracted JPEG files
|
|
88
|
+
def extract_jpeg_images(reader)
|
|
89
|
+
images = []
|
|
90
|
+
temp_dir = Dir.mktmpdir("pdf_#{Process.pid}")
|
|
91
|
+
|
|
92
|
+
reader.pages.each_with_index do |page, page_num|
|
|
93
|
+
page.xobjects.each do |name, stream|
|
|
94
|
+
next unless stream.hash[:Subtype] == :Image
|
|
95
|
+
next unless stream.hash[:Filter] == :DCTDecode # JPEG only
|
|
96
|
+
|
|
97
|
+
path = File.join(temp_dir, "p#{page_num + 1}_#{name}.jpg")
|
|
98
|
+
File.binwrite(path, stream.data)
|
|
99
|
+
images << path
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
images
|
|
104
|
+
rescue StandardError
|
|
105
|
+
[] # Silently ignore extraction failures
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
module DocumentConverters
|
|
7
|
+
# XLSX/Spreadsheet converter
|
|
8
|
+
#
|
|
9
|
+
# Converts spreadsheet files (XLSX, XLS, ODS) to CSV format.
|
|
10
|
+
# Requires the roo gem (and roo-xls for legacy XLS support).
|
|
11
|
+
class XlsxConverter < Base
|
|
12
|
+
class << self
|
|
13
|
+
# @return [String] gem name
|
|
14
|
+
def gem_name
|
|
15
|
+
"roo"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [String] format name
|
|
19
|
+
def format_name
|
|
20
|
+
"XLSX/Spreadsheet"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Array<String>] supported extensions
|
|
24
|
+
def extensions
|
|
25
|
+
[".xlsx", ".xls", ".ods"]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Convert spreadsheet to CSV text format
|
|
30
|
+
#
|
|
31
|
+
# @param file_path [String] path to spreadsheet file
|
|
32
|
+
# @return [String] CSV formatted text
|
|
33
|
+
def convert(file_path)
|
|
34
|
+
return unsupported_format_message unless self.class.available?
|
|
35
|
+
return unsupported_xls_message if file_path.end_with?(".xls") && !xls_available?
|
|
36
|
+
|
|
37
|
+
require "roo"
|
|
38
|
+
require "csv"
|
|
39
|
+
|
|
40
|
+
spreadsheet = Roo::Spreadsheet.open(file_path)
|
|
41
|
+
build_csv_output(spreadsheet, file_path)
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
error("Spreadsheet conversion failed: #{e.message}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# Check if roo-xls gem is available for legacy XLS support
|
|
49
|
+
#
|
|
50
|
+
# @return [Boolean] true if roo-xls is installed
|
|
51
|
+
def xls_available?
|
|
52
|
+
Gem::Specification.find_by_name("roo-xls")
|
|
53
|
+
true
|
|
54
|
+
rescue Gem::MissingSpecError
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Return system reminder for missing roo-xls gem
|
|
59
|
+
#
|
|
60
|
+
# @return [String] formatted system reminder
|
|
61
|
+
def unsupported_xls_message
|
|
62
|
+
<<~MSG.strip
|
|
63
|
+
<system-reminder>
|
|
64
|
+
Legacy XLS format requires additional gem.
|
|
65
|
+
|
|
66
|
+
To enable XLS support:
|
|
67
|
+
gem install roo-xls
|
|
68
|
+
|
|
69
|
+
Or save as .xlsx format.
|
|
70
|
+
</system-reminder>
|
|
71
|
+
MSG
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Build CSV output from spreadsheet
|
|
75
|
+
#
|
|
76
|
+
# @param spreadsheet [Roo::Spreadsheet] opened spreadsheet
|
|
77
|
+
# @param file_path [String] original file path
|
|
78
|
+
# @return [String] formatted CSV output
|
|
79
|
+
def build_csv_output(spreadsheet, file_path)
|
|
80
|
+
output = []
|
|
81
|
+
output << "Spreadsheet: #{File.basename(file_path)}"
|
|
82
|
+
output << "=" * 60
|
|
83
|
+
output << ""
|
|
84
|
+
|
|
85
|
+
spreadsheet.sheets.each do |sheet_name|
|
|
86
|
+
spreadsheet.default_sheet = sheet_name
|
|
87
|
+
rows = spreadsheet.last_row || 0
|
|
88
|
+
cols = spreadsheet.last_column || 0
|
|
89
|
+
|
|
90
|
+
output << "Sheet: #{sheet_name} (#{rows} rows × #{cols} cols)"
|
|
91
|
+
output << "-" * 60
|
|
92
|
+
|
|
93
|
+
# Stream rows for memory efficiency
|
|
94
|
+
spreadsheet.each_row_streaming do |row|
|
|
95
|
+
cells = row.map { |cell| format_cell(cell) }
|
|
96
|
+
output << CSV.generate_line(cells).chomp
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
output << ""
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
output.join("\n")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Format cell based on type
|
|
106
|
+
#
|
|
107
|
+
# @param cell [Roo::Cell] cell to format
|
|
108
|
+
# @return [String] formatted cell value
|
|
109
|
+
def format_cell(cell)
|
|
110
|
+
return "" if cell.nil? || cell.value.nil?
|
|
111
|
+
|
|
112
|
+
case cell.type
|
|
113
|
+
when :string then cell.value.to_s
|
|
114
|
+
when :float, :number then cell.value.to_s
|
|
115
|
+
when :date then cell.value.strftime("%Y-%m-%d")
|
|
116
|
+
when :datetime then cell.value.strftime("%Y-%m-%d %H:%M:%S")
|
|
117
|
+
when :time then cell.value.strftime("%H:%M:%S")
|
|
118
|
+
when :boolean then cell.value ? "TRUE" : "FALSE"
|
|
119
|
+
when :formula then cell.value.to_s # Calculated value
|
|
120
|
+
when :percentage then "#{(cell.value * 100).round(2)}%"
|
|
121
|
+
else cell.value.to_s
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
# Edit tool for performing exact string replacements in files
|
|
7
|
+
#
|
|
8
|
+
# Uses exact string matching to find and replace content.
|
|
9
|
+
# Requires the file to have been read first via the Read tool.
|
|
10
|
+
class Edit < Base
|
|
11
|
+
class << self
|
|
12
|
+
# @return [Array<Symbol>] Constructor requirements
|
|
13
|
+
def creation_requirements
|
|
14
|
+
[:agent_name, :directory, :read_tracker]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
description <<~DESC
|
|
19
|
+
Performs exact string replacements in files.
|
|
20
|
+
You must use Read on a file before editing it.
|
|
21
|
+
The edit will FAIL if old_string is not unique — provide more context or use replace_all.
|
|
22
|
+
|
|
23
|
+
Path handling:
|
|
24
|
+
- Relative paths resolve against your working directory
|
|
25
|
+
- Absolute paths (starting with /) are used as-is
|
|
26
|
+
DESC
|
|
27
|
+
|
|
28
|
+
param :file_path,
|
|
29
|
+
type: "string",
|
|
30
|
+
desc: "Path to the file to edit",
|
|
31
|
+
required: true
|
|
32
|
+
|
|
33
|
+
param :old_string,
|
|
34
|
+
type: "string",
|
|
35
|
+
desc: "The exact text to replace (must match exactly including whitespace)",
|
|
36
|
+
required: true
|
|
37
|
+
|
|
38
|
+
param :new_string,
|
|
39
|
+
type: "string",
|
|
40
|
+
desc: "The text to replace it with (must be different from old_string)",
|
|
41
|
+
required: true
|
|
42
|
+
|
|
43
|
+
param :replace_all,
|
|
44
|
+
type: "boolean",
|
|
45
|
+
desc: "Replace all occurrences of old_string (default false)",
|
|
46
|
+
required: false
|
|
47
|
+
|
|
48
|
+
# @param agent_name [Symbol, String] Agent identifier
|
|
49
|
+
# @param directory [String] Agent's working directory
|
|
50
|
+
# @param read_tracker [ReadTracker] Shared read tracker for enforcement
|
|
51
|
+
def initialize(agent_name:, directory:, read_tracker:)
|
|
52
|
+
super()
|
|
53
|
+
@agent_name = agent_name.to_sym
|
|
54
|
+
@directory = File.expand_path(directory)
|
|
55
|
+
@read_tracker = read_tracker
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Execute file edit
|
|
59
|
+
#
|
|
60
|
+
# @param file_path [String] Path to the file
|
|
61
|
+
# @param old_string [String] Text to find
|
|
62
|
+
# @param new_string [String] Replacement text
|
|
63
|
+
# @param replace_all [Boolean] Replace all occurrences
|
|
64
|
+
# @return [String] Success or error message
|
|
65
|
+
def execute(file_path:, old_string:, new_string:, replace_all: false)
|
|
66
|
+
return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
|
|
67
|
+
return validation_error("old_string is required") if old_string.nil? || old_string.empty?
|
|
68
|
+
return validation_error("new_string is required") if new_string.nil?
|
|
69
|
+
return validation_error("old_string and new_string must be different.") if old_string == new_string
|
|
70
|
+
|
|
71
|
+
resolved_path = resolve_path(file_path)
|
|
72
|
+
return validation_error("File does not exist: #{file_path}") unless File.exist?(resolved_path)
|
|
73
|
+
|
|
74
|
+
unless @read_tracker.file_read?(@agent_name, resolved_path)
|
|
75
|
+
return validation_error(
|
|
76
|
+
"Cannot edit file without reading it first. " \
|
|
77
|
+
"Use the Read tool on '#{file_path}' before editing.",
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
content = File.read(resolved_path, encoding: "UTF-8")
|
|
82
|
+
|
|
83
|
+
unless content.include?(old_string)
|
|
84
|
+
return validation_error("old_string not found in file. Make sure it matches exactly, including all whitespace and indentation.")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
occurrences = content.scan(old_string).count
|
|
88
|
+
|
|
89
|
+
if !replace_all && occurrences > 1
|
|
90
|
+
return validation_error(
|
|
91
|
+
"Found #{occurrences} occurrences of old_string. " \
|
|
92
|
+
"Provide more context to make the match unique, or use replace_all: true.",
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
new_content = replace_all ? content.gsub(old_string, new_string) : content.sub(old_string, new_string)
|
|
97
|
+
File.write(resolved_path, new_content, encoding: "UTF-8")
|
|
98
|
+
|
|
99
|
+
replaced_count = replace_all ? occurrences : 1
|
|
100
|
+
"Successfully replaced #{replaced_count} occurrence(s) in #{file_path}"
|
|
101
|
+
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
|
|
102
|
+
error("File contains invalid UTF-8. Cannot edit binary files.")
|
|
103
|
+
rescue Errno::EACCES
|
|
104
|
+
error("Permission denied: Cannot read or write file '#{file_path}'")
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
error("Unexpected error editing file: #{e.class.name} - #{e.message}")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
# Glob tool for fast file pattern matching
|
|
7
|
+
#
|
|
8
|
+
# Finds files and directories matching glob patterns, sorted by modification time.
|
|
9
|
+
class Glob < Base
|
|
10
|
+
class << self
|
|
11
|
+
# @return [Array<Symbol>] Constructor requirements
|
|
12
|
+
def creation_requirements
|
|
13
|
+
[:directory]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
description <<~DESC
|
|
18
|
+
Fast file pattern matching tool.
|
|
19
|
+
|
|
20
|
+
Supports glob patterns like "**/*.js" or "src/**/*.ts".
|
|
21
|
+
Returns matching file paths sorted by modification time (most recent first).
|
|
22
|
+
DESC
|
|
23
|
+
|
|
24
|
+
param :pattern,
|
|
25
|
+
type: "string",
|
|
26
|
+
desc: "The glob pattern to match files against",
|
|
27
|
+
required: true
|
|
28
|
+
|
|
29
|
+
param :path,
|
|
30
|
+
type: "string",
|
|
31
|
+
desc: "Directory to search in. Defaults to working directory.",
|
|
32
|
+
required: false
|
|
33
|
+
|
|
34
|
+
# @param directory [String] Working directory for pattern matching
|
|
35
|
+
def initialize(directory:)
|
|
36
|
+
super()
|
|
37
|
+
@directory = File.expand_path(directory)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Execute glob search
|
|
41
|
+
#
|
|
42
|
+
# @param pattern [String] Glob pattern
|
|
43
|
+
# @param path [String, nil] Search directory
|
|
44
|
+
# @return [String] Matching paths or error
|
|
45
|
+
def execute(pattern:, path: nil)
|
|
46
|
+
return validation_error("pattern is required") if pattern.nil? || pattern.to_s.strip.empty?
|
|
47
|
+
|
|
48
|
+
search_path = resolve_search_path(path)
|
|
49
|
+
return search_path if search_path.start_with?("<tool_use_error>")
|
|
50
|
+
|
|
51
|
+
full_pattern = pattern.start_with?("/") ? pattern : File.join(search_path, pattern)
|
|
52
|
+
matches = Dir.glob(full_pattern, File::FNM_DOTMATCH)
|
|
53
|
+
|
|
54
|
+
matches.reject! do |f|
|
|
55
|
+
basename = File.basename(f.chomp("/"))
|
|
56
|
+
basename == "." || basename == ".."
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return "No matches found for pattern: #{pattern}" if matches.empty?
|
|
60
|
+
|
|
61
|
+
matches.sort_by! { |f| -File.mtime(f).to_i }
|
|
62
|
+
|
|
63
|
+
max_results = Configuration.instance.glob_result_limit
|
|
64
|
+
truncated = matches.count > max_results
|
|
65
|
+
matches = matches.take(max_results) if truncated
|
|
66
|
+
|
|
67
|
+
output = matches.join("\n")
|
|
68
|
+
output += "\n\n<system-reminder>Results limited to first #{max_results} matches.</system-reminder>" if truncated
|
|
69
|
+
output
|
|
70
|
+
rescue Errno::EACCES => e
|
|
71
|
+
error("Permission denied: #{e.message}")
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
error("Failed to execute glob: #{e.class.name} - #{e.message}")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Resolve search path with validation
|
|
79
|
+
#
|
|
80
|
+
# @param path [String, nil] User-provided path
|
|
81
|
+
# @return [String] Resolved absolute path or validation error
|
|
82
|
+
def resolve_search_path(path)
|
|
83
|
+
if path && !path.to_s.strip.empty?
|
|
84
|
+
return validation_error("Invalid path value.") if ["undefined", "null"].include?(path.to_s.strip.downcase)
|
|
85
|
+
return validation_error("Path does not exist: #{path}") unless File.exist?(path)
|
|
86
|
+
return validation_error("Path is not a directory: #{path}") unless File.directory?(path)
|
|
87
|
+
|
|
88
|
+
resolve_path(path)
|
|
89
|
+
else
|
|
90
|
+
@directory
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|