claude_swarm 1.0.1 → 1.0.4
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 +14 -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 -2
- 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 +249 -6
- data/EXAMPLES.md +0 -164
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Tools
|
|
5
|
+
module Stores
|
|
6
|
+
# TodoManager provides per-agent todo list storage
|
|
7
|
+
#
|
|
8
|
+
# Each agent maintains its own independent todo list that persists
|
|
9
|
+
# throughout the agent's execution session. This allows agents to
|
|
10
|
+
# track progress on complex multi-step tasks.
|
|
11
|
+
class TodoManager
|
|
12
|
+
@storage = {}
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Get the current todo list for an agent
|
|
17
|
+
#
|
|
18
|
+
# @param agent_id [Symbol, String] Unique agent identifier
|
|
19
|
+
# @return [Array<Hash>] Array of todo items
|
|
20
|
+
def get_todos(agent_id)
|
|
21
|
+
@mutex.synchronize do
|
|
22
|
+
@storage[agent_id.to_sym] ||= []
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Set the todo list for an agent
|
|
27
|
+
#
|
|
28
|
+
# @param agent_id [Symbol, String] Unique agent identifier
|
|
29
|
+
# @param todos [Array<Hash>] Array of todo items
|
|
30
|
+
# @return [Array<Hash>] The stored todos
|
|
31
|
+
def set_todos(agent_id, todos)
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
@storage[agent_id.to_sym] = todos
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Clear all todos for an agent
|
|
38
|
+
#
|
|
39
|
+
# @param agent_id [Symbol, String] Unique agent identifier
|
|
40
|
+
def clear_todos(agent_id)
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
@storage.delete(agent_id.to_sym)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Clear all todos for all agents
|
|
47
|
+
def clear_all
|
|
48
|
+
@mutex.synchronize do
|
|
49
|
+
@storage.clear
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get summary of all agent todo lists
|
|
54
|
+
#
|
|
55
|
+
# @return [Hash] Map of agent_id => todo count
|
|
56
|
+
def summary
|
|
57
|
+
@mutex.synchronize do
|
|
58
|
+
@storage.transform_values(&:size)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Tools
|
|
5
|
+
# Think tool for explicit reasoning and planning
|
|
6
|
+
#
|
|
7
|
+
# Allows the agent to write down thoughts, plans, strategies, and intermediate
|
|
8
|
+
# calculations. These thoughts become part of the conversation context, enabling
|
|
9
|
+
# better attention and reasoning through complex problems.
|
|
10
|
+
#
|
|
11
|
+
# This is inspired by research showing that explicitly articulating reasoning steps
|
|
12
|
+
# (chain-of-thought prompting) leads to significantly better outcomes, especially
|
|
13
|
+
# for complex tasks requiring multi-step reasoning or arithmetic.
|
|
14
|
+
class Think < RubyLLM::Tool
|
|
15
|
+
def name
|
|
16
|
+
"Think"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
description <<~DESC
|
|
20
|
+
**IMPORTANT: You SHOULD use this tool frequently throughout your work. Using this tool leads to significantly
|
|
21
|
+
better outcomes and more accurate solutions. Make it a habit to think before acting.**
|
|
22
|
+
|
|
23
|
+
This tool allows you to write down your thoughts, plans, strategies, and intermediate calculations.
|
|
24
|
+
Think of it as your working memory - just as humans think before speaking or acting, you should think before
|
|
25
|
+
using other tools or providing responses.
|
|
26
|
+
|
|
27
|
+
**STRONGLY RECOMMENDED to use this tool:**
|
|
28
|
+
- **ALWAYS** before starting any task (even simple ones)
|
|
29
|
+
- **ALWAYS** when you need to do any arithmetic or counting
|
|
30
|
+
- **ALWAYS** after reading files or getting search results to process what you learned
|
|
31
|
+
- **FREQUENTLY** between steps to track progress and plan next actions
|
|
32
|
+
|
|
33
|
+
This is your private thinking space - use it liberally to enhance your problem-solving capabilities. Recording
|
|
34
|
+
your thoughts helps you maintain context across multiple steps and remember important information throughout your task.
|
|
35
|
+
|
|
36
|
+
When and how to use this tool:
|
|
37
|
+
|
|
38
|
+
1. **Before starting any complex task**: Write down your understanding of the problem, break it into smaller
|
|
39
|
+
sub-tasks, and create a step-by-step plan. Example:
|
|
40
|
+
- "The user wants me to refactor this codebase. Let me first understand the structure..."
|
|
41
|
+
- "I need to: 1) Analyze current architecture, 2) Identify pain points, 3) Propose changes..."
|
|
42
|
+
|
|
43
|
+
2. **For arithmetic and calculations**: Work through math problems step by step. Example:
|
|
44
|
+
- "If we have 150 requests/second and each takes 20ms, that's 150 * 0.02 = 3 seconds of CPU time..."
|
|
45
|
+
- "Converting 2GB to bytes: 2 * 1024 * 1024 * 1024 = 2,147,483,648 bytes"
|
|
46
|
+
|
|
47
|
+
3. **After completing sub-tasks**: Summarize what you've accomplished and what remains. Example:
|
|
48
|
+
- "I've successfully implemented the authentication module. Next, I need to integrate it with the API..."
|
|
49
|
+
- "Fixed 3 out of 5 bugs. Remaining: memory leak in parser, race condition in worker thread"
|
|
50
|
+
|
|
51
|
+
4. **When encountering complexity**: Break down complex logic or decisions. Example:
|
|
52
|
+
- "This function has multiple edge cases. Let me list them: null input, empty array, negative numbers..."
|
|
53
|
+
- "The user's request is ambiguous. Possible interpretations: A) modify existing code, B) create new module..."
|
|
54
|
+
|
|
55
|
+
5. **For remembering context**: Store important information you'll need later. Example:
|
|
56
|
+
- "Important: The user mentioned they're using Ruby 3.2, so I can use pattern matching"
|
|
57
|
+
- "File structure: main.rb requires from lib/, config is in config.yml"
|
|
58
|
+
|
|
59
|
+
6. **When debugging or analyzing**: Track your investigation process. Example:
|
|
60
|
+
- "The error occurs in line 42. Let me trace backwards: function called from main(), receives data from..."
|
|
61
|
+
- "Hypothesis: the bug might be due to timezone differences. Let me check..."
|
|
62
|
+
|
|
63
|
+
7. **For creative problem-solving**: Brainstorm multiple approaches before choosing one. Example:
|
|
64
|
+
- "Approaches to optimize this: 1) Add caching, 2) Use parallel processing, 3) Optimize algorithm..."
|
|
65
|
+
- "Design patterns that could work here: Factory, Observer, or maybe Strategy pattern..."
|
|
66
|
+
|
|
67
|
+
**Remember: The most successful agents use this tool 5-10 times per task on average. If you haven't used this
|
|
68
|
+
tool in the last 2-3 actions, you probably should. Using this tool is a sign of thoughtful, methodical problem
|
|
69
|
+
solving and leads to fewer mistakes and better solutions.**
|
|
70
|
+
|
|
71
|
+
Your thoughts persist throughout your session as part of the conversation history, so you can refer
|
|
72
|
+
back to earlier thinking. Use clear formatting and organization to make it easy to reference
|
|
73
|
+
later. Don't hesitate to think out loud - this tool is designed to augment your cognitive capabilities and help
|
|
74
|
+
you deliver better solutions.
|
|
75
|
+
|
|
76
|
+
**CRITICAL:** The Think tool takes only one parameter: thoughts. Do not include any other parameters.
|
|
77
|
+
DESC
|
|
78
|
+
|
|
79
|
+
param :thoughts,
|
|
80
|
+
type: "string",
|
|
81
|
+
desc: "Your thoughts, plans, calculations, or any notes you want to record",
|
|
82
|
+
required: true
|
|
83
|
+
|
|
84
|
+
def execute(**kwargs)
|
|
85
|
+
"Thought noted."
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def validation_error(message)
|
|
91
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Tools
|
|
5
|
+
# TodoWrite tool for creating and managing structured task lists
|
|
6
|
+
#
|
|
7
|
+
# This tool helps agents track progress on complex multi-step tasks.
|
|
8
|
+
# Each agent maintains its own independent todo list.
|
|
9
|
+
class TodoWrite < RubyLLM::Tool
|
|
10
|
+
description <<~DESC
|
|
11
|
+
Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
|
|
12
|
+
It also helps the user understand the progress of the task and overall progress of their requests.
|
|
13
|
+
|
|
14
|
+
## When to Use This Tool
|
|
15
|
+
Use this tool proactively in these scenarios:
|
|
16
|
+
|
|
17
|
+
**CRITICAL**: Follow this workflow for multi-step tasks:
|
|
18
|
+
1. FIRST: Analyze the task scope (search files, read code, understand requirements)
|
|
19
|
+
2. SECOND: Create a COMPLETE todo list with ALL known tasks BEFORE starting implementation
|
|
20
|
+
3. THIRD: Execute tasks, marking in_progress → completed as you work
|
|
21
|
+
4. ONLY add new todos if unexpected work is discovered during implementation
|
|
22
|
+
|
|
23
|
+
Use the todo list when:
|
|
24
|
+
1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions
|
|
25
|
+
2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations
|
|
26
|
+
3. User explicitly requests todo list - When the user directly asks you to use the todo list
|
|
27
|
+
4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)
|
|
28
|
+
5. After receiving new instructions - After analyzing scope, create complete todo list before starting work
|
|
29
|
+
6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time
|
|
30
|
+
7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation
|
|
31
|
+
|
|
32
|
+
## When NOT to Use This Tool
|
|
33
|
+
|
|
34
|
+
Skip using this tool when:
|
|
35
|
+
1. There is only a single, straightforward task
|
|
36
|
+
2. The task is trivial and tracking it provides no organizational benefit
|
|
37
|
+
3. The task can be completed in less than 3 trivial steps
|
|
38
|
+
4. The task is purely conversational or informational
|
|
39
|
+
|
|
40
|
+
NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.
|
|
41
|
+
|
|
42
|
+
## Task States and Management
|
|
43
|
+
|
|
44
|
+
1. **Task States**: Use these states to track progress:
|
|
45
|
+
- pending: Task not yet started
|
|
46
|
+
- in_progress: Currently working on (limit to ONE task at a time)
|
|
47
|
+
- completed: Task finished successfully
|
|
48
|
+
|
|
49
|
+
**IMPORTANT**: Task descriptions must have two forms:
|
|
50
|
+
- content: The imperative form describing what needs to be done (e.g., "Run tests", "Build the project")
|
|
51
|
+
- activeForm: The present continuous form shown during execution (e.g., "Running tests", "Building the project")
|
|
52
|
+
|
|
53
|
+
2. **Task Management**:
|
|
54
|
+
- Update task status in real-time as you work
|
|
55
|
+
- Mark tasks complete IMMEDIATELY after finishing (don't batch completions)
|
|
56
|
+
- Exactly ONE task must be in_progress at any time (not less, not more)
|
|
57
|
+
- Complete current tasks before starting new ones
|
|
58
|
+
- Remove tasks that are no longer relevant from the list entirely
|
|
59
|
+
- **CRITICAL**: You MUST complete ALL pending todos before giving your final answer to the user
|
|
60
|
+
- NEVER leave in_progress or pending tasks when you finish responding
|
|
61
|
+
|
|
62
|
+
3. **Task Completion Requirements**:
|
|
63
|
+
- ONLY mark a task as completed when you have FULLY accomplished it
|
|
64
|
+
- If you encounter errors, blockers, or cannot finish, keep the task as in_progress
|
|
65
|
+
- When blocked, create a new task describing what needs to be resolved
|
|
66
|
+
- Never mark a task as completed if:
|
|
67
|
+
- Tests are failing
|
|
68
|
+
- Implementation is partial
|
|
69
|
+
- You encountered unresolved errors
|
|
70
|
+
- You couldn't find necessary files or dependencies
|
|
71
|
+
|
|
72
|
+
4. **Task Breakdown**:
|
|
73
|
+
- Create specific, actionable items
|
|
74
|
+
- Break complex tasks into smaller, manageable steps
|
|
75
|
+
- Use clear, descriptive task names
|
|
76
|
+
- Always provide both forms:
|
|
77
|
+
- content: "Fix authentication bug"
|
|
78
|
+
- activeForm: "Fixing authentication bug"
|
|
79
|
+
|
|
80
|
+
When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.
|
|
81
|
+
DESC
|
|
82
|
+
|
|
83
|
+
param :todos_json,
|
|
84
|
+
type: "string",
|
|
85
|
+
desc: <<~DESC.chomp,
|
|
86
|
+
JSON array of todo objects. Each todo must have:
|
|
87
|
+
content (string, task in imperative form like 'Run tests'),
|
|
88
|
+
status (string, one of: 'pending', 'in_progress', 'completed'),
|
|
89
|
+
activeForm (string, task in present continuous form like 'Running tests').
|
|
90
|
+
Example: [{"content":"Read file","status":"pending","activeForm":"Reading file"}]
|
|
91
|
+
DESC
|
|
92
|
+
required: true
|
|
93
|
+
|
|
94
|
+
# Initialize the TodoWrite tool for a specific agent
|
|
95
|
+
#
|
|
96
|
+
# @param agent_name [Symbol, String] The agent identifier
|
|
97
|
+
def initialize(agent_name:)
|
|
98
|
+
super()
|
|
99
|
+
@agent_name = agent_name.to_sym
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Override name to return simple "TodoWrite" instead of full class path
|
|
103
|
+
def name
|
|
104
|
+
"TodoWrite"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def execute(todos_json:)
|
|
108
|
+
# Parse JSON
|
|
109
|
+
todos = begin
|
|
110
|
+
JSON.parse(todos_json)
|
|
111
|
+
rescue JSON::ParserError
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
return validation_error("Invalid JSON format. Please provide a valid JSON array of todo objects.") if todos.nil?
|
|
116
|
+
|
|
117
|
+
# Validate todos structure
|
|
118
|
+
unless todos.is_a?(Array)
|
|
119
|
+
return validation_error("todos must be an array of todo objects")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if todos.empty?
|
|
123
|
+
return validation_error("todos array cannot be empty")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
validated_todos = []
|
|
127
|
+
errors = []
|
|
128
|
+
|
|
129
|
+
todos.each_with_index do |todo, index|
|
|
130
|
+
unless todo.is_a?(Hash)
|
|
131
|
+
errors << "Todo at index #{index} must be a hash/object"
|
|
132
|
+
next
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Convert string keys to symbols for consistency
|
|
136
|
+
todo = todo.transform_keys(&:to_sym) if todo.is_a?(Hash)
|
|
137
|
+
|
|
138
|
+
# Validate required fields
|
|
139
|
+
unless todo[:content]
|
|
140
|
+
errors << "Todo at index #{index} missing required field 'content'"
|
|
141
|
+
next
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
unless todo[:status]
|
|
145
|
+
errors << "Todo at index #{index} missing required field 'status'"
|
|
146
|
+
next
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
unless todo[:activeForm]
|
|
150
|
+
errors << "Todo at index #{index} missing required field 'activeForm'"
|
|
151
|
+
next
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Validate status values
|
|
155
|
+
valid_statuses = ["pending", "in_progress", "completed"]
|
|
156
|
+
unless valid_statuses.include?(todo[:status].to_s)
|
|
157
|
+
errors << "Todo at index #{index} has invalid status '#{todo[:status]}'. Must be one of: #{valid_statuses.join(", ")}"
|
|
158
|
+
next
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Validate content and activeForm are non-empty
|
|
162
|
+
if todo[:content].to_s.strip.empty?
|
|
163
|
+
errors << "Todo at index #{index} has empty content"
|
|
164
|
+
next
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
if todo[:activeForm].to_s.strip.empty?
|
|
168
|
+
errors << "Todo at index #{index} has empty activeForm"
|
|
169
|
+
next
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
validated_todos << {
|
|
173
|
+
content: todo[:content].to_s,
|
|
174
|
+
status: todo[:status].to_s,
|
|
175
|
+
activeForm: todo[:activeForm].to_s,
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
return validation_error("TodoWrite failed due to the following issues:\n#{errors.join("\n")}") unless errors.empty?
|
|
180
|
+
|
|
181
|
+
# Check that exactly one task is in_progress (with helpful message)
|
|
182
|
+
in_progress_count = validated_todos.count { |t| t[:status] == "in_progress" }
|
|
183
|
+
warning_message = if in_progress_count == 0
|
|
184
|
+
"Warning: No tasks marked as in_progress. You should have exactly ONE task in_progress at a time.\n" \
|
|
185
|
+
"Please mark the task you're currently working on as in_progress.\n\n"
|
|
186
|
+
elsif in_progress_count > 1
|
|
187
|
+
"Warning: Multiple tasks marked as in_progress (#{in_progress_count} tasks).\n" \
|
|
188
|
+
"You should have exactly ONE task in_progress at a time.\n" \
|
|
189
|
+
"Please ensure only the current task is in_progress, others should be pending or completed.\n\n"
|
|
190
|
+
else
|
|
191
|
+
""
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Store the validated todos
|
|
195
|
+
Stores::TodoManager.set_todos(@agent_name, validated_todos)
|
|
196
|
+
|
|
197
|
+
<<~RESPONSE
|
|
198
|
+
<system-reminder>
|
|
199
|
+
#{warning_message}Your todo list has changed. DO NOT mention this explicitly to the user. Here are the latest contents of your todo list:
|
|
200
|
+
#{validated_todos.map { |t| "- #{t[:content]} (#{t[:status]})" }.join("\n")}
|
|
201
|
+
Keep going with the tasks at hand if applicable.
|
|
202
|
+
</system-reminder>
|
|
203
|
+
RESPONSE
|
|
204
|
+
rescue StandardError => e
|
|
205
|
+
"Error managing todos: #{e.class.name} - #{e.message}"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
# Helper method for validation errors
|
|
211
|
+
def validation_error(message)
|
|
212
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Tools
|
|
5
|
+
# WebFetch tool for fetching and processing web content
|
|
6
|
+
#
|
|
7
|
+
# Fetches content from URLs, converts HTML to markdown, and processes it
|
|
8
|
+
# using an AI model to extract information based on a provided prompt.
|
|
9
|
+
class WebFetch < RubyLLM::Tool
|
|
10
|
+
def initialize
|
|
11
|
+
super()
|
|
12
|
+
@cache = {}
|
|
13
|
+
@cache_ttl = 900 # 15 minutes in seconds
|
|
14
|
+
@llm_enabled = SwarmSDK.settings.webfetch_llm_enabled?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def name
|
|
18
|
+
"WebFetch"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
description <<~DESC
|
|
22
|
+
- Fetches content from a specified URL and converts it to markdown
|
|
23
|
+
- Optionally processes the content with an LLM if configured
|
|
24
|
+
- Fetches the URL content, converts HTML to markdown
|
|
25
|
+
- Returns markdown content or LLM analysis (based on configuration)
|
|
26
|
+
- Use this tool when you need to retrieve and analyze web content
|
|
27
|
+
|
|
28
|
+
Usage notes:
|
|
29
|
+
- IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with "mcp__".
|
|
30
|
+
- The URL must be a fully-formed valid URL
|
|
31
|
+
- HTTP URLs will be automatically upgraded to HTTPS
|
|
32
|
+
- This tool is read-only and does not modify any files
|
|
33
|
+
- Content will be truncated if very large
|
|
34
|
+
- Includes a self-cleaning 15-minute cache for faster responses
|
|
35
|
+
- When a URL redirects to a different host, the tool will inform you and provide the redirect URL in a special format. You should then make a new WebFetch request with the redirect URL to fetch the content.
|
|
36
|
+
|
|
37
|
+
LLM Processing:
|
|
38
|
+
- When SwarmSDK is configured with webfetch_provider and webfetch_model, the 'prompt' parameter is required
|
|
39
|
+
- The tool will process the markdown content with the configured LLM using your prompt
|
|
40
|
+
- Without this configuration, the tool returns raw markdown and the 'prompt' parameter is optional (ignored if provided)
|
|
41
|
+
- Configure with: SwarmSDK.configure { |c| c.webfetch_provider = "anthropic"; c.webfetch_model = "claude-3-5-haiku-20241022" }
|
|
42
|
+
DESC
|
|
43
|
+
|
|
44
|
+
param :url,
|
|
45
|
+
type: "string",
|
|
46
|
+
desc: "The URL to fetch content from",
|
|
47
|
+
required: true
|
|
48
|
+
|
|
49
|
+
param :prompt,
|
|
50
|
+
type: "string",
|
|
51
|
+
desc: "The prompt to run on the fetched content. Required when SwarmSDK is configured with webfetch_provider and webfetch_model. Optional otherwise (ignored if LLM processing not configured).",
|
|
52
|
+
required: false
|
|
53
|
+
|
|
54
|
+
MAX_CONTENT_LENGTH = 100_000 # characters
|
|
55
|
+
USER_AGENT = "SwarmSDK WebFetch Tool (https://github.com/parruda/claude-swarm)"
|
|
56
|
+
TIMEOUT = 30 # seconds
|
|
57
|
+
|
|
58
|
+
def execute(url:, prompt: nil)
|
|
59
|
+
# Validate inputs
|
|
60
|
+
return validation_error("url is required") if url.nil? || url.empty?
|
|
61
|
+
|
|
62
|
+
# Validate prompt when LLM processing is enabled
|
|
63
|
+
if @llm_enabled && (prompt.nil? || prompt.empty?)
|
|
64
|
+
return validation_error("prompt is required when LLM processing is configured")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Validate and normalize URL
|
|
68
|
+
normalized_url = normalize_url(url)
|
|
69
|
+
return validation_error("Invalid URL format: #{url}") unless normalized_url
|
|
70
|
+
|
|
71
|
+
# Check cache first (cache key includes prompt if LLM is enabled)
|
|
72
|
+
cache_key = @llm_enabled ? "#{normalized_url}:#{prompt}" : normalized_url
|
|
73
|
+
cached = get_from_cache(cache_key)
|
|
74
|
+
return cached if cached
|
|
75
|
+
|
|
76
|
+
# Fetch the URL
|
|
77
|
+
fetch_result = fetch_url(normalized_url)
|
|
78
|
+
return fetch_result if fetch_result.is_a?(String) && fetch_result.start_with?("Error")
|
|
79
|
+
|
|
80
|
+
# Check for redirects to different hosts
|
|
81
|
+
if fetch_result[:redirect_url] && different_host?(normalized_url, fetch_result[:redirect_url])
|
|
82
|
+
return format_redirect_message(fetch_result[:redirect_url])
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Convert HTML to markdown
|
|
86
|
+
markdown_content = html_to_markdown(fetch_result[:body])
|
|
87
|
+
|
|
88
|
+
# Truncate if too long
|
|
89
|
+
if markdown_content.length > MAX_CONTENT_LENGTH
|
|
90
|
+
markdown_content = markdown_content[0...MAX_CONTENT_LENGTH]
|
|
91
|
+
markdown_content += "\n\n[Content truncated due to length]"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Process with AI model if LLM is enabled, otherwise return markdown
|
|
95
|
+
result = if @llm_enabled
|
|
96
|
+
process_with_llm(markdown_content, prompt, normalized_url)
|
|
97
|
+
else
|
|
98
|
+
markdown_content
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Cache the result
|
|
102
|
+
store_in_cache(cache_key, result)
|
|
103
|
+
|
|
104
|
+
result
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
error("Unexpected error fetching URL: #{e.class.name} - #{e.message}")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def validation_error(message)
|
|
112
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def error(message)
|
|
116
|
+
"Error: #{message}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def normalize_url(url)
|
|
120
|
+
# Upgrade HTTP to HTTPS
|
|
121
|
+
url = url.sub(%r{^http://}, "https://")
|
|
122
|
+
|
|
123
|
+
# Validate URL format
|
|
124
|
+
uri = URI.parse(url)
|
|
125
|
+
return unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
126
|
+
return unless uri.host
|
|
127
|
+
|
|
128
|
+
uri.to_s
|
|
129
|
+
rescue URI::InvalidURIError
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def different_host?(url1, url2)
|
|
134
|
+
uri1 = URI.parse(url1)
|
|
135
|
+
uri2 = URI.parse(url2)
|
|
136
|
+
uri1.host != uri2.host
|
|
137
|
+
rescue URI::InvalidURIError
|
|
138
|
+
false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def fetch_url(url)
|
|
142
|
+
require "faraday"
|
|
143
|
+
require "faraday/follow_redirects"
|
|
144
|
+
|
|
145
|
+
response = Faraday.new(url: url) do |conn|
|
|
146
|
+
conn.request(:url_encoded)
|
|
147
|
+
conn.response(:follow_redirects, limit: 5)
|
|
148
|
+
conn.adapter(Faraday.default_adapter)
|
|
149
|
+
conn.options.timeout = TIMEOUT
|
|
150
|
+
conn.options.open_timeout = TIMEOUT
|
|
151
|
+
end.get do |req|
|
|
152
|
+
req.headers["User-Agent"] = USER_AGENT
|
|
153
|
+
req.headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
unless response.success?
|
|
157
|
+
return error("HTTP #{response.status}: Failed to fetch URL")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Check final URL for redirects
|
|
161
|
+
final_url = response.env.url.to_s
|
|
162
|
+
redirect_url = final_url if final_url != url
|
|
163
|
+
|
|
164
|
+
{
|
|
165
|
+
body: response.body,
|
|
166
|
+
redirect_url: redirect_url,
|
|
167
|
+
}
|
|
168
|
+
rescue Faraday::TimeoutError
|
|
169
|
+
error("Request timed out after #{TIMEOUT} seconds")
|
|
170
|
+
rescue Faraday::ConnectionFailed => e
|
|
171
|
+
error("Connection failed: #{e.message}")
|
|
172
|
+
rescue StandardError => e
|
|
173
|
+
error("Failed to fetch URL: #{e.class.name} - #{e.message}")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def html_to_markdown(html)
|
|
177
|
+
# Use HtmlConverter to handle conversion with optional reverse_markdown gem
|
|
178
|
+
converter = DocumentConverters::HtmlConverter.new
|
|
179
|
+
converter.convert_string(html)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def process_with_llm(content, prompt, url)
|
|
183
|
+
# Use configured model for processing
|
|
184
|
+
# Format the prompt to include the content
|
|
185
|
+
full_prompt = <<~PROMPT
|
|
186
|
+
You are analyzing content from the URL: #{url}
|
|
187
|
+
|
|
188
|
+
User request: #{prompt}
|
|
189
|
+
|
|
190
|
+
Content:
|
|
191
|
+
#{content}
|
|
192
|
+
|
|
193
|
+
Please respond to the user's request based on the content above.
|
|
194
|
+
PROMPT
|
|
195
|
+
|
|
196
|
+
# Get settings
|
|
197
|
+
config = SwarmSDK.settings
|
|
198
|
+
|
|
199
|
+
# Build chat with configured provider and model
|
|
200
|
+
chat_params = {
|
|
201
|
+
model: config.webfetch_model,
|
|
202
|
+
provider: config.webfetch_provider.to_sym,
|
|
203
|
+
}
|
|
204
|
+
chat_params[:base_url] = config.webfetch_base_url if config.webfetch_base_url
|
|
205
|
+
|
|
206
|
+
chat = RubyLLM.chat(**chat_params).with_params(max_tokens: config.webfetch_max_tokens)
|
|
207
|
+
|
|
208
|
+
response = chat.ask(full_prompt)
|
|
209
|
+
|
|
210
|
+
# Extract the text response
|
|
211
|
+
response_text = response.content
|
|
212
|
+
return error("Failed to process content with LLM: No response text") unless response_text
|
|
213
|
+
|
|
214
|
+
response_text
|
|
215
|
+
rescue StandardError => e
|
|
216
|
+
error("Failed to process content with LLM: #{e.class.name} - #{e.message}")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def format_redirect_message(redirect_url)
|
|
220
|
+
<<~MESSAGE
|
|
221
|
+
This URL redirected to a different host.
|
|
222
|
+
|
|
223
|
+
Redirect URL: #{redirect_url}
|
|
224
|
+
|
|
225
|
+
<system-reminder>
|
|
226
|
+
The requested URL redirected to a different host. To fetch the content from the redirect URL,
|
|
227
|
+
make a new WebFetch request with the redirect URL provided above.
|
|
228
|
+
</system-reminder>
|
|
229
|
+
MESSAGE
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def get_from_cache(key)
|
|
233
|
+
entry = @cache[key]
|
|
234
|
+
return unless entry
|
|
235
|
+
|
|
236
|
+
# Check if cache entry is still valid
|
|
237
|
+
if Time.now.to_i - entry[:timestamp] > @cache_ttl
|
|
238
|
+
@cache.delete(key)
|
|
239
|
+
return
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
entry[:value]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def store_in_cache(key, value)
|
|
246
|
+
# Clean old cache entries
|
|
247
|
+
clean_cache
|
|
248
|
+
|
|
249
|
+
@cache[key] = {
|
|
250
|
+
value: value,
|
|
251
|
+
timestamp: Time.now.to_i,
|
|
252
|
+
}
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def clean_cache
|
|
256
|
+
now = Time.now.to_i
|
|
257
|
+
@cache.delete_if { |_key, entry| now - entry[:timestamp] > @cache_ttl }
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|