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
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
# Delegate tool for working with other agents in the swarm
|
|
6
|
-
#
|
|
7
|
-
# Creates agent-specific collaboration tools (e.g., WorkWithBackend)
|
|
8
|
-
# that allow one agent to work with another agent.
|
|
9
|
-
# Supports pre/post delegation hooks for customization.
|
|
10
|
-
class Delegate < Base
|
|
11
|
-
removable true # Delegate tools can be controlled by skills
|
|
12
|
-
# Tool name prefix for delegation tools
|
|
13
|
-
# Change this to customize the tool naming pattern (e.g., "DelegateTaskTo", "AskAgent", etc.)
|
|
14
|
-
TOOL_NAME_PREFIX = "WorkWith"
|
|
15
|
-
|
|
16
|
-
class << self
|
|
17
|
-
# Generate tool name for a delegate agent
|
|
18
|
-
#
|
|
19
|
-
# This is the single source of truth for delegation tool naming.
|
|
20
|
-
# Used both when creating Delegate instances and when predicting tool names
|
|
21
|
-
# for agent context setup.
|
|
22
|
-
#
|
|
23
|
-
# Converts names to PascalCase: backend → Backend, slack_agent → SlackAgent
|
|
24
|
-
#
|
|
25
|
-
# @param delegate_name [String, Symbol] Name of the delegate agent
|
|
26
|
-
# @return [String] Tool name (e.g., "WorkWithBackend", "WorkWithSlackAgent")
|
|
27
|
-
#
|
|
28
|
-
# @example Simple name
|
|
29
|
-
# tool_name_for(:backend) # => "WorkWithBackend"
|
|
30
|
-
#
|
|
31
|
-
# @example Name with underscore
|
|
32
|
-
# tool_name_for(:slack_agent) # => "WorkWithSlackAgent"
|
|
33
|
-
def tool_name_for(delegate_name)
|
|
34
|
-
# Convert to PascalCase: split on underscore, capitalize each part, join
|
|
35
|
-
pascal_case = delegate_name.to_s.split("_").map(&:capitalize).join
|
|
36
|
-
"#{TOOL_NAME_PREFIX}#{pascal_case}"
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
attr_reader :delegate_name, :delegate_target, :tool_name, :preserve_context, :delegate_chat
|
|
41
|
-
|
|
42
|
-
# Initialize a delegation tool
|
|
43
|
-
#
|
|
44
|
-
# @param delegate_name [String] Name of the delegate agent (e.g., "backend")
|
|
45
|
-
# @param delegate_description [String] Description of the delegate agent
|
|
46
|
-
# @param delegate_chat [AgentChat, nil] The chat instance for the delegate agent (nil if delegating to swarm)
|
|
47
|
-
# @param agent_name [Symbol, String] Name of the agent using this tool
|
|
48
|
-
# @param swarm [Swarm] The swarm instance (provides hook_registry, swarm_registry)
|
|
49
|
-
# @param delegating_chat [Agent::Chat, nil] The chat instance of the agent doing the delegating (for accessing hooks)
|
|
50
|
-
# @param custom_tool_name [String, nil] Optional custom tool name (overrides auto-generated name)
|
|
51
|
-
# @param preserve_context [Boolean] Whether to preserve conversation context between delegations (default: true)
|
|
52
|
-
def initialize(
|
|
53
|
-
delegate_name:,
|
|
54
|
-
delegate_description:,
|
|
55
|
-
delegate_chat:,
|
|
56
|
-
agent_name:,
|
|
57
|
-
swarm:,
|
|
58
|
-
delegating_chat: nil,
|
|
59
|
-
custom_tool_name: nil,
|
|
60
|
-
preserve_context: true
|
|
61
|
-
)
|
|
62
|
-
super()
|
|
63
|
-
|
|
64
|
-
@delegate_name = delegate_name
|
|
65
|
-
@delegate_description = delegate_description
|
|
66
|
-
@delegate_chat = delegate_chat
|
|
67
|
-
@agent_name = agent_name
|
|
68
|
-
@swarm = swarm
|
|
69
|
-
@delegating_chat = delegating_chat
|
|
70
|
-
@preserve_context = preserve_context
|
|
71
|
-
|
|
72
|
-
# Use custom tool name if provided, otherwise generate using canonical method
|
|
73
|
-
@tool_name = custom_tool_name || self.class.tool_name_for(delegate_name)
|
|
74
|
-
@delegate_target = delegate_name.to_s
|
|
75
|
-
|
|
76
|
-
# Track concurrent delegations to this target.
|
|
77
|
-
# When multiple parallel tool calls target the same delegate, only the first
|
|
78
|
-
# preserves context; subsequent concurrent calls always clear context to
|
|
79
|
-
# prevent cross-contamination between independent parallel work.
|
|
80
|
-
#
|
|
81
|
-
# No Mutex needed: Async Fibers run on a single thread and only switch at
|
|
82
|
-
# explicit yield points (IO, sleep, semaphore.acquire). Integer increment
|
|
83
|
-
# and decrement never yield, so they are inherently atomic.
|
|
84
|
-
@active_count = 0
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Override description to return dynamic string based on delegate
|
|
88
|
-
def description
|
|
89
|
-
"Work with #{@delegate_name} to delegate work, ask questions, or collaborate. #{@delegate_description}"
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
param :message,
|
|
93
|
-
type: "string",
|
|
94
|
-
desc: "Message to send to the agent - can be a work request, question, or collaboration message",
|
|
95
|
-
required: true
|
|
96
|
-
|
|
97
|
-
param :reset_context,
|
|
98
|
-
type: "boolean",
|
|
99
|
-
desc: "Reset the agent's conversation history before sending the message. Use it to recover from 'prompt too long' errors or other 4XX errors.",
|
|
100
|
-
required: false
|
|
101
|
-
|
|
102
|
-
# Override name to return custom delegation tool name
|
|
103
|
-
def name
|
|
104
|
-
@tool_name
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Check if this delegate uses lazy loading
|
|
108
|
-
#
|
|
109
|
-
# @return [Boolean] True if delegate is lazy-loaded
|
|
110
|
-
def lazy?
|
|
111
|
-
@delegate_chat.is_a?(Swarm::LazyDelegateChat)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Check if this delegate has been initialized
|
|
115
|
-
#
|
|
116
|
-
# @return [Boolean] True if delegate chat is ready (either eager or lazy-initialized)
|
|
117
|
-
def initialized?
|
|
118
|
-
return true unless lazy?
|
|
119
|
-
|
|
120
|
-
@delegate_chat.initialized?
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Force initialization of lazy delegate
|
|
124
|
-
#
|
|
125
|
-
# If the delegate is lazy-loaded, this will trigger immediate initialization.
|
|
126
|
-
# For eager delegates, this is a no-op.
|
|
127
|
-
#
|
|
128
|
-
# @return [Agent::Chat] The resolved chat instance
|
|
129
|
-
def initialize_delegate!
|
|
130
|
-
resolve_delegate_chat
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Execute delegation with pre/post hooks
|
|
134
|
-
#
|
|
135
|
-
# Uses Fiber-local path tracking for circular dependency detection.
|
|
136
|
-
# Each concurrent delegation runs in its own Fiber (via Async), so the path
|
|
137
|
-
# is isolated per execution path. This correctly distinguishes parallel fan-out
|
|
138
|
-
# (A→B, A→B) from true circular dependencies (A→B→A).
|
|
139
|
-
#
|
|
140
|
-
# @param message [String] Message to send to the agent
|
|
141
|
-
# @param reset_context [Boolean] Whether to reset the agent's conversation history before delegation
|
|
142
|
-
# @return [String] Result from delegate agent or error message
|
|
143
|
-
def execute(message:, reset_context: false)
|
|
144
|
-
# Save the current delegation path so we can restore it after execution.
|
|
145
|
-
# The extended path (with our target) is only needed during chat.ask() so
|
|
146
|
-
# child Fibers (nested delegations) inherit it. After delegation returns,
|
|
147
|
-
# this Fiber's path should be unchanged.
|
|
148
|
-
saved_delegation_path = Fiber[:delegation_path]
|
|
149
|
-
|
|
150
|
-
# Access swarm infrastructure
|
|
151
|
-
hook_registry = @swarm.hook_registry
|
|
152
|
-
swarm_registry = @swarm.swarm_registry
|
|
153
|
-
|
|
154
|
-
# Check for circular dependency using Fiber-local path
|
|
155
|
-
# Each Fiber inherits the parent's path, so nested delegations
|
|
156
|
-
# accumulate the full chain while parallel siblings remain isolated
|
|
157
|
-
delegation_path = saved_delegation_path || []
|
|
158
|
-
if delegation_path.include?(@delegate_target)
|
|
159
|
-
emit_circular_warning(delegation_path)
|
|
160
|
-
return "Error: Circular delegation detected: #{delegation_path.join(" -> ")} -> #{@delegate_target}. " \
|
|
161
|
-
"Please restructure your delegation to avoid infinite loops."
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# Get agent-specific hooks from the delegating chat instance
|
|
165
|
-
agent_hooks = if @delegating_chat&.respond_to?(:hook_agent_hooks)
|
|
166
|
-
@delegating_chat.hook_agent_hooks || {}
|
|
167
|
-
else
|
|
168
|
-
{}
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Trigger pre_delegation callback
|
|
172
|
-
context = Hooks::Context.new(
|
|
173
|
-
event: :pre_delegation,
|
|
174
|
-
agent_name: @agent_name,
|
|
175
|
-
swarm: @swarm,
|
|
176
|
-
delegation_target: @delegate_target,
|
|
177
|
-
metadata: {
|
|
178
|
-
tool_name: @tool_name,
|
|
179
|
-
message: message,
|
|
180
|
-
timestamp: Time.now.utc.iso8601,
|
|
181
|
-
},
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
executor = Hooks::Executor.new(hook_registry, logger: RubyLLM.logger)
|
|
185
|
-
pre_agent_hooks = agent_hooks[:pre_delegation] || []
|
|
186
|
-
result = executor.execute_safe(event: :pre_delegation, context: context, callbacks: pre_agent_hooks)
|
|
187
|
-
|
|
188
|
-
# Check if callback halted or replaced the delegation
|
|
189
|
-
if result.halt?
|
|
190
|
-
return result.value || "Delegation halted by callback"
|
|
191
|
-
elsif result.replace?
|
|
192
|
-
return result.value
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
# Determine delegation type and proceed
|
|
196
|
-
delegation_result = if @delegate_chat
|
|
197
|
-
# Delegate to agent
|
|
198
|
-
delegate_to_agent(message, reset_context: reset_context)
|
|
199
|
-
elsif swarm_registry&.registered?(@delegate_target)
|
|
200
|
-
# Delegate to registered swarm
|
|
201
|
-
delegate_to_swarm(message, swarm_registry, reset_context: reset_context)
|
|
202
|
-
else
|
|
203
|
-
raise ConfigurationError, "Unknown delegation target: #{@delegate_target}"
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
# Trigger post_delegation callback
|
|
207
|
-
post_context = Hooks::Context.new(
|
|
208
|
-
event: :post_delegation,
|
|
209
|
-
agent_name: @agent_name,
|
|
210
|
-
swarm: @swarm,
|
|
211
|
-
delegation_target: @delegate_target,
|
|
212
|
-
delegation_result: delegation_result,
|
|
213
|
-
metadata: {
|
|
214
|
-
tool_name: @tool_name,
|
|
215
|
-
message: message,
|
|
216
|
-
result: delegation_result,
|
|
217
|
-
timestamp: Time.now.utc.iso8601,
|
|
218
|
-
},
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
post_agent_hooks = agent_hooks[:post_delegation] || []
|
|
222
|
-
post_result = executor.execute_safe(event: :post_delegation, context: post_context, callbacks: post_agent_hooks)
|
|
223
|
-
|
|
224
|
-
# Return modified result if callback replaces it
|
|
225
|
-
if post_result.replace?
|
|
226
|
-
post_result.value
|
|
227
|
-
else
|
|
228
|
-
delegation_result
|
|
229
|
-
end
|
|
230
|
-
rescue Faraday::TimeoutError, Net::ReadTimeout => e
|
|
231
|
-
# Log timeout error as JSON event
|
|
232
|
-
LogStream.emit(
|
|
233
|
-
type: "delegation_error",
|
|
234
|
-
agent: @agent_name,
|
|
235
|
-
swarm_id: @swarm.swarm_id,
|
|
236
|
-
parent_swarm_id: @swarm.parent_swarm_id,
|
|
237
|
-
delegate_to: @tool_name,
|
|
238
|
-
error_class: e.class.name,
|
|
239
|
-
error_message: "Request timed out",
|
|
240
|
-
error_backtrace: e.backtrace&.first(5) || [],
|
|
241
|
-
)
|
|
242
|
-
"Error: Request to #{@tool_name} timed out. The agent may be overloaded or the LLM service is not responding. Please try again or simplify the task."
|
|
243
|
-
rescue Faraday::Error => e
|
|
244
|
-
# Log network error as JSON event
|
|
245
|
-
LogStream.emit(
|
|
246
|
-
type: "delegation_error",
|
|
247
|
-
agent: @agent_name,
|
|
248
|
-
swarm_id: @swarm.swarm_id,
|
|
249
|
-
parent_swarm_id: @swarm.parent_swarm_id,
|
|
250
|
-
delegate_to: @tool_name,
|
|
251
|
-
error_class: e.class.name,
|
|
252
|
-
error_message: e.message,
|
|
253
|
-
error_backtrace: e.backtrace&.first(5) || [],
|
|
254
|
-
)
|
|
255
|
-
"Error: Network error communicating with #{@tool_name}: #{e.class.name}. Please check connectivity and try again."
|
|
256
|
-
rescue StandardError => e
|
|
257
|
-
# Log unexpected error as JSON event
|
|
258
|
-
backtrace_array = e.backtrace&.first(5) || []
|
|
259
|
-
LogStream.emit(
|
|
260
|
-
type: "delegation_error",
|
|
261
|
-
agent: @agent_name,
|
|
262
|
-
swarm_id: @swarm.swarm_id,
|
|
263
|
-
parent_swarm_id: @swarm.parent_swarm_id,
|
|
264
|
-
delegate_to: @tool_name,
|
|
265
|
-
error_class: e.class.name,
|
|
266
|
-
error_message: e.message,
|
|
267
|
-
error_backtrace: backtrace_array,
|
|
268
|
-
)
|
|
269
|
-
# Return error string for LLM
|
|
270
|
-
backtrace_str = backtrace_array.join("\n ")
|
|
271
|
-
"Error: #{@tool_name} encountered an error: #{e.class.name}: #{e.message}\nBacktrace:\n #{backtrace_str}"
|
|
272
|
-
ensure
|
|
273
|
-
# Restore the calling Fiber's delegation path.
|
|
274
|
-
# The extended path was only needed during chat.ask() so child Fibers
|
|
275
|
-
# (spawned for nested tool calls) could inherit it for circular detection.
|
|
276
|
-
Fiber[:delegation_path] = saved_delegation_path
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
private
|
|
280
|
-
|
|
281
|
-
# Delegate to an agent
|
|
282
|
-
#
|
|
283
|
-
# Handles both eager Agent::Chat instances and lazy-loaded delegates.
|
|
284
|
-
# LazyDelegateChat instances are initialized on first access.
|
|
285
|
-
# Sets Fiber-local delegation path so child Fibers (nested delegations)
|
|
286
|
-
# inherit the full chain for circular dependency detection.
|
|
287
|
-
#
|
|
288
|
-
# Tracks concurrent delegations to this target. When multiple parallel
|
|
289
|
-
# tool calls target the same delegate (fan-out), only the first call
|
|
290
|
-
# preserves context; subsequent concurrent calls always clear context
|
|
291
|
-
# to prevent cross-contamination between independent parallel work.
|
|
292
|
-
# Context clearing happens inside Agent::Chat's ask_semaphore for safety.
|
|
293
|
-
#
|
|
294
|
-
# @param message [String] Message to send to the agent
|
|
295
|
-
# @param reset_context [Boolean] Whether to reset the agent's conversation history before delegation
|
|
296
|
-
# @return [String] Result from agent
|
|
297
|
-
def delegate_to_agent(message, reset_context: false)
|
|
298
|
-
@active_count += 1
|
|
299
|
-
concurrent = @active_count > 1
|
|
300
|
-
|
|
301
|
-
# Set Fiber-local delegation path for this execution path
|
|
302
|
-
# Child Fibers (from nested delegations) inherit this path automatically
|
|
303
|
-
# We create a new array to avoid mutating the parent Fiber's reference
|
|
304
|
-
Fiber[:delegation_path] = (Fiber[:delegation_path] || []) + [@delegate_target]
|
|
305
|
-
|
|
306
|
-
# Resolve the chat instance (handles lazy loading)
|
|
307
|
-
chat = resolve_delegate_chat
|
|
308
|
-
|
|
309
|
-
# Determine if context should be cleared:
|
|
310
|
-
# - reset_context: explicit caller request
|
|
311
|
-
# - !preserve_context: agent configuration
|
|
312
|
-
# - concurrent: parallel fan-out to same delegate (always isolate)
|
|
313
|
-
# Clearing is done inside chat.ask's semaphore to avoid race conditions
|
|
314
|
-
should_clear = reset_context || !@preserve_context || concurrent
|
|
315
|
-
|
|
316
|
-
response = chat.ask(message, source: "delegation", clear_context: should_clear)
|
|
317
|
-
response.content
|
|
318
|
-
ensure
|
|
319
|
-
@active_count -= 1
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
# Resolve the delegate chat instance
|
|
323
|
-
#
|
|
324
|
-
# If the delegate is a LazyDelegateChat, initializes it on first access.
|
|
325
|
-
# Otherwise, returns the chat directly.
|
|
326
|
-
#
|
|
327
|
-
# @return [Agent::Chat] The resolved chat instance
|
|
328
|
-
def resolve_delegate_chat
|
|
329
|
-
if @delegate_chat.is_a?(Swarm::LazyDelegateChat)
|
|
330
|
-
@delegate_chat.chat
|
|
331
|
-
else
|
|
332
|
-
@delegate_chat
|
|
333
|
-
end
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
# Delegate to a registered swarm
|
|
337
|
-
#
|
|
338
|
-
# Sets Fiber-local delegation path so child Fibers (nested delegations)
|
|
339
|
-
# inherit the full chain for circular dependency detection.
|
|
340
|
-
# Tracks concurrent delegations the same way as delegate_to_agent.
|
|
341
|
-
#
|
|
342
|
-
# @param message [String] Message to send to the swarm
|
|
343
|
-
# @param swarm_registry [SwarmRegistry] Registry for sub-swarms
|
|
344
|
-
# @param reset_context [Boolean] Whether to reset the swarm's conversation history before delegation
|
|
345
|
-
# @return [String] Result from swarm's lead agent
|
|
346
|
-
def delegate_to_swarm(message, swarm_registry, reset_context: false)
|
|
347
|
-
@active_count += 1
|
|
348
|
-
concurrent = @active_count > 1
|
|
349
|
-
|
|
350
|
-
# Set Fiber-local delegation path for this execution path
|
|
351
|
-
Fiber[:delegation_path] = (Fiber[:delegation_path] || []) + [@delegate_target]
|
|
352
|
-
|
|
353
|
-
# Load sub-swarm (lazy load + cache)
|
|
354
|
-
subswarm = swarm_registry.load_swarm(@delegate_target)
|
|
355
|
-
|
|
356
|
-
# Reset swarm context if explicitly requested or concurrent fan-out
|
|
357
|
-
swarm_registry.reset(@delegate_target) if reset_context || concurrent
|
|
358
|
-
|
|
359
|
-
# Execute sub-swarm's lead agent (uses agent() to trigger lazy initialization)
|
|
360
|
-
lead_agent = subswarm.agent(subswarm.lead_agent)
|
|
361
|
-
response = lead_agent.ask(message, source: "delegation")
|
|
362
|
-
result = response.content
|
|
363
|
-
|
|
364
|
-
# Reset if keep_context: false (standard behavior)
|
|
365
|
-
swarm_registry.reset_if_needed(@delegate_target)
|
|
366
|
-
|
|
367
|
-
result
|
|
368
|
-
ensure
|
|
369
|
-
@active_count -= 1
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
# Emit circular dependency warning event
|
|
373
|
-
#
|
|
374
|
-
# @param delegation_path [Array<String>] Current Fiber-local delegation path
|
|
375
|
-
# @return [void]
|
|
376
|
-
def emit_circular_warning(delegation_path)
|
|
377
|
-
LogStream.emit(
|
|
378
|
-
type: "delegation_circular_dependency",
|
|
379
|
-
agent: @agent_name,
|
|
380
|
-
swarm_id: @swarm.swarm_id,
|
|
381
|
-
parent_swarm_id: @swarm.parent_swarm_id,
|
|
382
|
-
target: @delegate_target,
|
|
383
|
-
delegation_path: delegation_path,
|
|
384
|
-
timestamp: Time.now.utc.iso8601,
|
|
385
|
-
)
|
|
386
|
-
end
|
|
387
|
-
end
|
|
388
|
-
end
|
|
389
|
-
end
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
module DocumentConverters
|
|
6
|
-
# Base class for document converters
|
|
7
|
-
# Provides common interface and utility methods for converting various document formats
|
|
8
|
-
class BaseConverter
|
|
9
|
-
class << self
|
|
10
|
-
# The gem name required for this converter
|
|
11
|
-
# @return [String]
|
|
12
|
-
def gem_name
|
|
13
|
-
raise NotImplementedError, "#{name} must implement .gem_name"
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Human-readable format name
|
|
17
|
-
# @return [String]
|
|
18
|
-
def format_name
|
|
19
|
-
raise NotImplementedError, "#{name} must implement .format_name"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# File extensions this converter handles
|
|
23
|
-
# @return [Array<String>]
|
|
24
|
-
def extensions
|
|
25
|
-
raise NotImplementedError, "#{name} must implement .extensions"
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Check if the required gem is available
|
|
29
|
-
# @return [Boolean]
|
|
30
|
-
def available?
|
|
31
|
-
gem_available?(gem_name)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Check if a gem is installed
|
|
35
|
-
# @param gem_name [String] Name of the gem to check
|
|
36
|
-
# @return [Boolean]
|
|
37
|
-
def gem_available?(gem_name)
|
|
38
|
-
Gem::Specification.find_by_name(gem_name)
|
|
39
|
-
true
|
|
40
|
-
rescue Gem::LoadError
|
|
41
|
-
false
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Convert a document file to text/content
|
|
46
|
-
# @param file_path [String] Path to the file
|
|
47
|
-
# @return [String, RubyLLM::Content] Converted content or error message
|
|
48
|
-
def convert(file_path)
|
|
49
|
-
raise NotImplementedError, "#{self.class.name} must implement #convert"
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
protected
|
|
53
|
-
|
|
54
|
-
# Return a system reminder about missing gem
|
|
55
|
-
# @param format [String] Format name (e.g., "PDF")
|
|
56
|
-
# @param gem_name [String] Required gem name
|
|
57
|
-
# @return [String]
|
|
58
|
-
def unsupported_format_reminder(format, gem_name)
|
|
59
|
-
<<~REMINDER
|
|
60
|
-
<system-reminder>
|
|
61
|
-
This file is a #{format} document, but the required gem is not installed.
|
|
62
|
-
|
|
63
|
-
To enable #{format} file reading, please install the gem:
|
|
64
|
-
gem install #{gem_name}
|
|
65
|
-
|
|
66
|
-
Or add to your Gemfile:
|
|
67
|
-
gem "#{gem_name}"
|
|
68
|
-
|
|
69
|
-
Don't install the gem yourself. Ask the user if they would like you to install this gem.
|
|
70
|
-
</system-reminder>
|
|
71
|
-
REMINDER
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Return an error message
|
|
75
|
-
# @param message [String] Error message
|
|
76
|
-
# @return [String]
|
|
77
|
-
def error(message)
|
|
78
|
-
"Error: #{message}"
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
module DocumentConverters
|
|
6
|
-
# Converts DOCX documents to text with image extraction
|
|
7
|
-
class DocxConverter < BaseConverter
|
|
8
|
-
class << self
|
|
9
|
-
def gem_name
|
|
10
|
-
"docx"
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def format_name
|
|
14
|
-
"DOCX"
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def extensions
|
|
18
|
-
[".docx", ".doc"]
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Convert a DOCX document to text/content
|
|
23
|
-
# @param file_path [String] Path to the DOCX file
|
|
24
|
-
# @return [String, RubyLLM::Content] Converted content or error message
|
|
25
|
-
def convert(file_path)
|
|
26
|
-
unless self.class.available?
|
|
27
|
-
return unsupported_format_reminder(self.class.format_name, self.class.gem_name)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Check for legacy DOC format
|
|
31
|
-
if File.extname(file_path).downcase == ".doc"
|
|
32
|
-
return error("DOC format is not supported. Please convert to DOCX first.")
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
begin
|
|
36
|
-
require "docx"
|
|
37
|
-
require "tmpdir"
|
|
38
|
-
|
|
39
|
-
doc = Docx::Document.open(file_path)
|
|
40
|
-
|
|
41
|
-
# Extract images from the DOCX
|
|
42
|
-
image_paths = ImageExtractors::DocxImageExtractor.extract_images(doc, file_path)
|
|
43
|
-
|
|
44
|
-
output = []
|
|
45
|
-
output << "Document: #{File.basename(file_path)}"
|
|
46
|
-
output << "=" * 60
|
|
47
|
-
output << ""
|
|
48
|
-
|
|
49
|
-
# Extract paragraphs
|
|
50
|
-
paragraphs = doc.paragraphs.map(&:text).reject(&:empty?)
|
|
51
|
-
|
|
52
|
-
# Check for empty document
|
|
53
|
-
if paragraphs.empty? && doc.tables.empty?
|
|
54
|
-
output << "(Document is empty - no paragraphs or tables)"
|
|
55
|
-
else
|
|
56
|
-
output += paragraphs
|
|
57
|
-
|
|
58
|
-
# Extract tables with enhanced formatting
|
|
59
|
-
if doc.tables.any?
|
|
60
|
-
output << ""
|
|
61
|
-
output << "Tables:"
|
|
62
|
-
output << "-" * 60
|
|
63
|
-
|
|
64
|
-
doc.tables.each_with_index do |table, idx|
|
|
65
|
-
output << ""
|
|
66
|
-
output << "Table #{idx + 1} (#{table.row_count} rows × #{table.column_count} columns):"
|
|
67
|
-
|
|
68
|
-
table.rows.each do |row|
|
|
69
|
-
output << row.cells.map(&:text).join(" | ")
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
text_content = output.join("\n")
|
|
76
|
-
|
|
77
|
-
# If there are images, return Content with attachments
|
|
78
|
-
if image_paths.any?
|
|
79
|
-
content = RubyLLM::Content.new(text_content)
|
|
80
|
-
image_paths.each do |image_path|
|
|
81
|
-
content.add_attachment(image_path)
|
|
82
|
-
end
|
|
83
|
-
content
|
|
84
|
-
else
|
|
85
|
-
# No images, return just text
|
|
86
|
-
text_content
|
|
87
|
-
end
|
|
88
|
-
rescue Zip::Error => e
|
|
89
|
-
error("Invalid or corrupted DOCX file: #{e.message}")
|
|
90
|
-
rescue Errno::ENOENT => e
|
|
91
|
-
error("File not found or missing document.xml: #{e.message}")
|
|
92
|
-
rescue StandardError => e
|
|
93
|
-
error("Failed to parse DOCX file: #{e.message}")
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
module DocumentConverters
|
|
6
|
-
# Converter for HTML to Markdown
|
|
7
|
-
# Uses reverse_markdown gem if available, otherwise falls back to simple regex-based conversion
|
|
8
|
-
class HtmlConverter < BaseConverter
|
|
9
|
-
class << self
|
|
10
|
-
def gem_name
|
|
11
|
-
"reverse_markdown"
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def format_name
|
|
15
|
-
"HTML"
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def extensions
|
|
19
|
-
[".html", ".htm"]
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Convert HTML string to Markdown
|
|
24
|
-
# @param html [String] HTML content to convert
|
|
25
|
-
# @return [String] Markdown content
|
|
26
|
-
def convert_string(html)
|
|
27
|
-
if self.class.available?
|
|
28
|
-
convert_with_gem(html)
|
|
29
|
-
else
|
|
30
|
-
convert_simple(html)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Convert HTML file to Markdown
|
|
35
|
-
# @param file_path [String] Path to HTML file
|
|
36
|
-
# @return [String] Markdown content
|
|
37
|
-
def convert(file_path)
|
|
38
|
-
html = File.read(file_path)
|
|
39
|
-
convert_string(html)
|
|
40
|
-
rescue StandardError => e
|
|
41
|
-
error("Failed to read HTML file: #{e.message}")
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
# Convert HTML to Markdown using reverse_markdown gem
|
|
47
|
-
# @param html [String] HTML content
|
|
48
|
-
# @return [String] Markdown content
|
|
49
|
-
def convert_with_gem(html)
|
|
50
|
-
require "reverse_markdown"
|
|
51
|
-
|
|
52
|
-
ReverseMarkdown.convert(html, unknown_tags: :bypass, github_flavored: true)
|
|
53
|
-
rescue StandardError
|
|
54
|
-
# Fallback to simple conversion if gem conversion fails
|
|
55
|
-
convert_simple(html)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Simple regex-based HTML to Markdown conversion (fallback)
|
|
59
|
-
# @param html [String] HTML content
|
|
60
|
-
# @return [String] Markdown content
|
|
61
|
-
def convert_simple(html)
|
|
62
|
-
# Remove script and style tags
|
|
63
|
-
content = html.gsub(%r{<script[^>]*>.*?</script>}im, "")
|
|
64
|
-
content = content.gsub(%r{<style[^>]*>.*?</style>}im, "")
|
|
65
|
-
|
|
66
|
-
# Convert common HTML elements
|
|
67
|
-
content = content.gsub(%r{<h1[^>]*>(.*?)</h1>}im, "\n# \\1\n")
|
|
68
|
-
content = content.gsub(%r{<h2[^>]*>(.*?)</h2>}im, "\n## \\1\n")
|
|
69
|
-
content = content.gsub(%r{<h3[^>]*>(.*?)</h3>}im, "\n### \\1\n")
|
|
70
|
-
content = content.gsub(%r{<h4[^>]*>(.*?)</h4>}im, "\n#### \\1\n")
|
|
71
|
-
content = content.gsub(%r{<h5[^>]*>(.*?)</h5>}im, "\n##### \\1\n")
|
|
72
|
-
content = content.gsub(%r{<h6[^>]*>(.*?)</h6>}im, "\n###### \\1\n")
|
|
73
|
-
content = content.gsub(%r{<p[^>]*>(.*?)</p>}im, "\n\\1\n")
|
|
74
|
-
content = content.gsub(%r{<br\s*/?>}i, "\n")
|
|
75
|
-
content = content.gsub(%r{<strong[^>]*>(.*?)</strong>}im, "**\\1**")
|
|
76
|
-
content = content.gsub(%r{<b[^>]*>(.*?)</b>}im, "**\\1**")
|
|
77
|
-
content = content.gsub(%r{<em[^>]*>(.*?)</em>}im, "_\\1_")
|
|
78
|
-
content = content.gsub(%r{<i[^>]*>(.*?)</i>}im, "_\\1_")
|
|
79
|
-
content = content.gsub(%r{<code[^>]*>(.*?)</code>}im, "`\\1`")
|
|
80
|
-
content = content.gsub(%r{<a[^>]*href=["']([^"']*)["'][^>]*>(.*?)</a>}im, "[\\2](\\1)")
|
|
81
|
-
content = content.gsub(%r{<li[^>]*>(.*?)</li>}im, "- \\1\n")
|
|
82
|
-
|
|
83
|
-
# Remove remaining HTML tags
|
|
84
|
-
content = content.gsub(/<[^>]+>/, "")
|
|
85
|
-
|
|
86
|
-
# Decode HTML entities
|
|
87
|
-
content = content.gsub("<", "<")
|
|
88
|
-
content = content.gsub(">", ">")
|
|
89
|
-
content = content.gsub("&", "&")
|
|
90
|
-
content = content.gsub(""", "\"")
|
|
91
|
-
content = content.gsub("'", "'")
|
|
92
|
-
content = content.gsub(" ", " ")
|
|
93
|
-
|
|
94
|
-
# Clean up whitespace
|
|
95
|
-
content = content.gsub(/\n\n\n+/, "\n\n")
|
|
96
|
-
content.strip
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|