rubino-agent 0.3.0
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +115 -0
- data/.rubocop_todo.yml +955 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +97 -0
- data/CHANGELOG.md +344 -0
- data/CONTRIBUTING.md +69 -0
- data/LICENSE +21 -0
- data/README.md +200 -0
- data/Rakefile +8 -0
- data/docs/agents.md +190 -0
- data/docs/api/v1.md +414 -0
- data/docs/architecture.md +177 -0
- data/docs/commands.md +375 -0
- data/docs/configuration.md +590 -0
- data/docs/getting-started.md +143 -0
- data/docs/jobs.md +332 -0
- data/docs/mcp.md +128 -0
- data/docs/memory.md +98 -0
- data/docs/models-and-keys.md +173 -0
- data/docs/oauth-providers.md +145 -0
- data/docs/plugins.md +195 -0
- data/docs/security.md +145 -0
- data/docs/skills.md +322 -0
- data/docs/tools.md +395 -0
- data/docs/troubleshooting.md +73 -0
- data/exe/rubino +9 -0
- data/install.sh +275 -0
- data/lib/rubino/active_skill.rb +50 -0
- data/lib/rubino/agent/agent_registry.rb +120 -0
- data/lib/rubino/agent/backoff_policy.rb +116 -0
- data/lib/rubino/agent/definition.rb +128 -0
- data/lib/rubino/agent/degenerate_recovery.rb +271 -0
- data/lib/rubino/agent/fallback_chain.rb +194 -0
- data/lib/rubino/agent/iteration_budget.rb +50 -0
- data/lib/rubino/agent/loop.rb +617 -0
- data/lib/rubino/agent/model_call_runner.rb +383 -0
- data/lib/rubino/agent/prompts/build.txt +69 -0
- data/lib/rubino/agent/prompts/compaction.txt +20 -0
- data/lib/rubino/agent/prompts/explore.txt +19 -0
- data/lib/rubino/agent/prompts/general.txt +20 -0
- data/lib/rubino/agent/prompts/plan.txt +31 -0
- data/lib/rubino/agent/response_validator.rb +70 -0
- data/lib/rubino/agent/router.rb +65 -0
- data/lib/rubino/agent/runner.rb +195 -0
- data/lib/rubino/agent/tool_executor.rb +402 -0
- data/lib/rubino/agent/truncation_continuation.rb +137 -0
- data/lib/rubino/api/middleware/auth.rb +43 -0
- data/lib/rubino/api/middleware/error_handler.rb +65 -0
- data/lib/rubino/api/middleware/json_parser.rb +100 -0
- data/lib/rubino/api/middleware/observability.rb +59 -0
- data/lib/rubino/api/middleware/rate_limit.rb +136 -0
- data/lib/rubino/api/operations/approvals/decide_operation.rb +49 -0
- data/lib/rubino/api/operations/clarifications/decide_operation.rb +44 -0
- data/lib/rubino/api/operations/cron_jobs/create_operation.rb +46 -0
- data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +36 -0
- data/lib/rubino/api/operations/cron_jobs/list_operation.rb +55 -0
- data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/schedule_validation.rb +30 -0
- data/lib/rubino/api/operations/cron_jobs/show_operation.rb +32 -0
- data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +38 -0
- data/lib/rubino/api/operations/cron_jobs/update_operation.rb +42 -0
- data/lib/rubino/api/operations/files/read_operation.rb +40 -0
- data/lib/rubino/api/operations/files/upload_operation.rb +175 -0
- data/lib/rubino/api/operations/health_operation.rb +46 -0
- data/lib/rubino/api/operations/memory/delete_operation.rb +32 -0
- data/lib/rubino/api/operations/memory/index_operation.rb +80 -0
- data/lib/rubino/api/operations/memory/stats_operation.rb +28 -0
- data/lib/rubino/api/operations/metrics_operation.rb +18 -0
- data/lib/rubino/api/operations/mode/show_operation.rb +29 -0
- data/lib/rubino/api/operations/mode/update_operation.rb +42 -0
- data/lib/rubino/api/operations/models/list_operation.rb +45 -0
- data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +77 -0
- data/lib/rubino/api/operations/oauth/connections/list_operation.rb +36 -0
- data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +82 -0
- data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +44 -0
- data/lib/rubino/api/operations/oauth/providers/list_operation.rb +35 -0
- data/lib/rubino/api/operations/oauth/serializer.rb +21 -0
- data/lib/rubino/api/operations/runs/create_operation.rb +77 -0
- data/lib/rubino/api/operations/runs/events_operation.rb +195 -0
- data/lib/rubino/api/operations/runs/stop_operation.rb +34 -0
- data/lib/rubino/api/operations/sessions/create_operation.rb +46 -0
- data/lib/rubino/api/operations/sessions/delete_operation.rb +33 -0
- data/lib/rubino/api/operations/sessions/index_operation.rb +82 -0
- data/lib/rubino/api/operations/sessions/retry_operation.rb +45 -0
- data/lib/rubino/api/operations/sessions/show_operation.rb +59 -0
- data/lib/rubino/api/operations/sessions/undo_operation.rb +38 -0
- data/lib/rubino/api/operations/skills/list_operation.rb +34 -0
- data/lib/rubino/api/operations/skills/toggle_operation.rb +40 -0
- data/lib/rubino/api/operations/tasks/index_operation.rb +30 -0
- data/lib/rubino/api/operations/tasks/serializer.rb +60 -0
- data/lib/rubino/api/operations/tasks/show_operation.rb +33 -0
- data/lib/rubino/api/operations/tasks/stop_operation.rb +47 -0
- data/lib/rubino/api/request.rb +54 -0
- data/lib/rubino/api/responses.rb +64 -0
- data/lib/rubino/api/router.rb +72 -0
- data/lib/rubino/api/schemas.rb +103 -0
- data/lib/rubino/api/server.rb +102 -0
- data/lib/rubino/api/tls.rb +108 -0
- data/lib/rubino/attachments/classification.rb +16 -0
- data/lib/rubino/attachments/classify.rb +171 -0
- data/lib/rubino/attachments/defang.rb +47 -0
- data/lib/rubino/attachments/policy.rb +36 -0
- data/lib/rubino/attachments/preamble.rb +120 -0
- data/lib/rubino/boot/encryption_key.rb +32 -0
- data/lib/rubino/cli/chat/bang_shell.rb +257 -0
- data/lib/rubino/cli/chat/completion_builder.rb +290 -0
- data/lib/rubino/cli/chat/idle_card_host.rb +69 -0
- data/lib/rubino/cli/chat/image_inbox.rb +168 -0
- data/lib/rubino/cli/chat/session_resolver.rb +176 -0
- data/lib/rubino/cli/chat_command.rb +1674 -0
- data/lib/rubino/cli/commands.rb +250 -0
- data/lib/rubino/cli/config_command.rb +96 -0
- data/lib/rubino/cli/doctor_command.rb +251 -0
- data/lib/rubino/cli/jobs_command.rb +60 -0
- data/lib/rubino/cli/memory_command.rb +135 -0
- data/lib/rubino/cli/onboarding_wizard.rb +207 -0
- data/lib/rubino/cli/server_command.rb +139 -0
- data/lib/rubino/cli/session_command.rb +125 -0
- data/lib/rubino/cli/setup_command.rb +107 -0
- data/lib/rubino/cli/skills_command.rb +85 -0
- data/lib/rubino/cli/tools_command.rb +81 -0
- data/lib/rubino/cli/trust_gate.rb +71 -0
- data/lib/rubino/commands/built_ins.rb +46 -0
- data/lib/rubino/commands/command.rb +116 -0
- data/lib/rubino/commands/executor.rb +550 -0
- data/lib/rubino/commands/handlers/agents.rb +510 -0
- data/lib/rubino/commands/handlers/config.rb +88 -0
- data/lib/rubino/commands/handlers/help.rb +148 -0
- data/lib/rubino/commands/handlers/jobs.rb +71 -0
- data/lib/rubino/commands/handlers/mcp.rb +229 -0
- data/lib/rubino/commands/handlers/memory.rb +200 -0
- data/lib/rubino/commands/handlers/sessions.rb +207 -0
- data/lib/rubino/commands/handlers/skills.rb +195 -0
- data/lib/rubino/commands/handlers/status.rb +211 -0
- data/lib/rubino/commands/loader.rb +90 -0
- data/lib/rubino/config/configuration.rb +455 -0
- data/lib/rubino/config/defaults.rb +569 -0
- data/lib/rubino/config/loader.rb +115 -0
- data/lib/rubino/config/reasoning_prefs.rb +67 -0
- data/lib/rubino/config/writer.rb +72 -0
- data/lib/rubino/context/compressor.rb +149 -0
- data/lib/rubino/context/environment_inspector.rb +176 -0
- data/lib/rubino/context/file_discovery.rb +45 -0
- data/lib/rubino/context/message_boundary.rb +39 -0
- data/lib/rubino/context/prompt_assembler.rb +382 -0
- data/lib/rubino/context/summary_builder.rb +159 -0
- data/lib/rubino/context/token_budget.rb +68 -0
- data/lib/rubino/context/tool_pair_sanitizer.rb +70 -0
- data/lib/rubino/database/connection.rb +77 -0
- data/lib/rubino/database/migrations/001_create_initial_schema.rb +156 -0
- data/lib/rubino/database/migrations/002_create_runs.rb +45 -0
- data/lib/rubino/database/migrations/003_create_skill_states.rb +15 -0
- data/lib/rubino/database/migrations/004_create_cron_jobs.rb +36 -0
- data/lib/rubino/database/migrations/005_create_oauth_connections.rb +27 -0
- data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +34 -0
- data/lib/rubino/database/migrations/007_create_messages_fts.rb +59 -0
- data/lib/rubino/database/migrations/008_create_memory_facts.rb +75 -0
- data/lib/rubino/database/migrations/009_create_memory_graph.rb +55 -0
- data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +20 -0
- data/lib/rubino/database/migrator.rb +48 -0
- data/lib/rubino/documents/converters/csv.rb +79 -0
- data/lib/rubino/documents/converters/docx.rb +129 -0
- data/lib/rubino/documents/converters/html.rb +28 -0
- data/lib/rubino/documents/converters/json.rb +35 -0
- data/lib/rubino/documents/converters/pdf.rb +59 -0
- data/lib/rubino/documents/converters/plain.rb +68 -0
- data/lib/rubino/documents/converters/pptx.rb +64 -0
- data/lib/rubino/documents/converters/xlsx.rb +62 -0
- data/lib/rubino/documents/converters/xml.rb +45 -0
- data/lib/rubino/documents/html.rb +71 -0
- data/lib/rubino/documents/registry.rb +68 -0
- data/lib/rubino/documents/table.rb +63 -0
- data/lib/rubino/documents.rb +50 -0
- data/lib/rubino/errors.rb +119 -0
- data/lib/rubino/files/workspace.rb +93 -0
- data/lib/rubino/interaction/cancel_token.rb +43 -0
- data/lib/rubino/interaction/clipboard_image.rb +84 -0
- data/lib/rubino/interaction/event_bus.rb +48 -0
- data/lib/rubino/interaction/events.rb +101 -0
- data/lib/rubino/interaction/image_input.rb +127 -0
- data/lib/rubino/interaction/input_queue.rb +117 -0
- data/lib/rubino/interaction/lifecycle.rb +299 -0
- data/lib/rubino/interaction/probe.rb +65 -0
- data/lib/rubino/interaction/state.rb +56 -0
- data/lib/rubino/jobs/cron_job_repository.rb +75 -0
- data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +32 -0
- data/lib/rubino/jobs/handlers/compact_session_job.rb +21 -0
- data/lib/rubino/jobs/handlers/distill_skill_job.rb +186 -0
- data/lib/rubino/jobs/handlers/extract_memory_job.rb +37 -0
- data/lib/rubino/jobs/handlers/summarize_session_job.rb +21 -0
- data/lib/rubino/jobs/queue.rb +184 -0
- data/lib/rubino/jobs/registry.rb +45 -0
- data/lib/rubino/jobs/runner.rb +79 -0
- data/lib/rubino/jobs/scheduler.rb +138 -0
- data/lib/rubino/jobs/webhook_delivery.rb +225 -0
- data/lib/rubino/jobs/worker.rb +59 -0
- data/lib/rubino/llm/adapter_factory.rb +47 -0
- data/lib/rubino/llm/adapter_response.rb +65 -0
- data/lib/rubino/llm/auxiliary_client.rb +61 -0
- data/lib/rubino/llm/bedrock_bearer_client.rb +235 -0
- data/lib/rubino/llm/content_builder.rb +55 -0
- data/lib/rubino/llm/credential_check.rb +93 -0
- data/lib/rubino/llm/error_classifier.rb +364 -0
- data/lib/rubino/llm/fake_provider.rb +292 -0
- data/lib/rubino/llm/inline_think_filter.rb +58 -0
- data/lib/rubino/llm/model_catalog.rb +29 -0
- data/lib/rubino/llm/provider_resolver.rb +48 -0
- data/lib/rubino/llm/reasoning_manager.rb +100 -0
- data/lib/rubino/llm/request.rb +56 -0
- data/lib/rubino/llm/ruby_llm_adapter.rb +794 -0
- data/lib/rubino/llm/scenario_loader.rb +68 -0
- data/lib/rubino/llm/scenario_selector.rb +80 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron-failure.yml +29 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron.yml +36 -0
- data/lib/rubino/llm/scenarios/analysis.yml +501 -0
- data/lib/rubino/llm/scenarios/complex-analysis.yml +598 -0
- data/lib/rubino/llm/scenarios/failure.yml +65 -0
- data/lib/rubino/llm/scenarios/happy-path.yml +24 -0
- data/lib/rubino/llm/scenarios/provider-quota-completed.yml +14 -0
- data/lib/rubino/llm/scenarios/wide-table.yml +121 -0
- data/lib/rubino/llm/scenarios/with-approvals.yml +50 -0
- data/lib/rubino/llm/scenarios/with-artifacts.yml +98 -0
- data/lib/rubino/llm/scenarios/with-clarify.yml +32 -0
- data/lib/rubino/llm/scenarios/with-reasoning.yml +175 -0
- data/lib/rubino/llm/scenarios/with-uploads.yml +104 -0
- data/lib/rubino/llm/thinking_support.rb +84 -0
- data/lib/rubino/llm/tool_bridge.rb +89 -0
- data/lib/rubino/logger.rb +99 -0
- data/lib/rubino/mcp/manager.rb +180 -0
- data/lib/rubino/mcp/mcp_tool_wrapper.rb +69 -0
- data/lib/rubino/mcp.rb +57 -0
- data/lib/rubino/memory/backend.rb +104 -0
- data/lib/rubino/memory/backends/default.rb +101 -0
- data/lib/rubino/memory/backends/sqlite.rb +653 -0
- data/lib/rubino/memory/backends.rb +53 -0
- data/lib/rubino/memory/deduplicator.rb +74 -0
- data/lib/rubino/memory/extractor.rb +85 -0
- data/lib/rubino/memory/flusher.rb +31 -0
- data/lib/rubino/memory/retriever.rb +50 -0
- data/lib/rubino/memory/sqlite_extraction_prompt.rb +70 -0
- data/lib/rubino/memory/sqlite_graph.rb +154 -0
- data/lib/rubino/memory/store.rb +228 -0
- data/lib/rubino/memory/threat_scanner.rb +68 -0
- data/lib/rubino/metrics.rb +175 -0
- data/lib/rubino/modes.rb +93 -0
- data/lib/rubino/oauth/connection_repository.rb +95 -0
- data/lib/rubino/oauth/provider/github.rb +75 -0
- data/lib/rubino/oauth/provider/google.rb +59 -0
- data/lib/rubino/oauth/provider.rb +149 -0
- data/lib/rubino/oauth/registry.rb +86 -0
- data/lib/rubino/oauth/token_encryptor.rb +87 -0
- data/lib/rubino/plugins/registry.rb +75 -0
- data/lib/rubino/plugins.rb +86 -0
- data/lib/rubino/run/approval_gate.rb +243 -0
- data/lib/rubino/run/attachment_downloader.rb +166 -0
- data/lib/rubino/run/event_store.rb +74 -0
- data/lib/rubino/run/executor.rb +383 -0
- data/lib/rubino/run/gate_registry.rb +39 -0
- data/lib/rubino/run/recorder.rb +69 -0
- data/lib/rubino/run/repository.rb +118 -0
- data/lib/rubino/run/session_approval_cache.rb +118 -0
- data/lib/rubino/security/allowlist_persister.rb +55 -0
- data/lib/rubino/security/approval_policy.rb +227 -0
- data/lib/rubino/security/command_allowlist.rb +24 -0
- data/lib/rubino/security/dangerous_patterns.rb +118 -0
- data/lib/rubino/security/deny_persister.rb +73 -0
- data/lib/rubino/security/doom_loop_detector.rb +43 -0
- data/lib/rubino/security/hardline_guard.rb +105 -0
- data/lib/rubino/security/pattern_matcher.rb +62 -0
- data/lib/rubino/security/prefix_deriver.rb +124 -0
- data/lib/rubino/security/readonly_commands.rb +211 -0
- data/lib/rubino/session/exporter.rb +101 -0
- data/lib/rubino/session/message.rb +77 -0
- data/lib/rubino/session/repository.rb +295 -0
- data/lib/rubino/session/store.rb +198 -0
- data/lib/rubino/session/summary_store.rb +65 -0
- data/lib/rubino/skills/prompt_index.rb +85 -0
- data/lib/rubino/skills/registry.rb +208 -0
- data/lib/rubino/skills/skill.rb +176 -0
- data/lib/rubino/skills/skill_tool.rb +215 -0
- data/lib/rubino/skills/state_repository.rb +37 -0
- data/lib/rubino/skills/toggle.rb +26 -0
- data/lib/rubino/tools/answer_child_tool.rb +83 -0
- data/lib/rubino/tools/ask_parent_tool.rb +232 -0
- data/lib/rubino/tools/attach_file_tool.rb +120 -0
- data/lib/rubino/tools/background_tasks.rb +520 -0
- data/lib/rubino/tools/base.rb +222 -0
- data/lib/rubino/tools/custom_tool_loader.rb +119 -0
- data/lib/rubino/tools/edit_tool.rb +122 -0
- data/lib/rubino/tools/git_tool.rb +71 -0
- data/lib/rubino/tools/github_tool.rb +233 -0
- data/lib/rubino/tools/glob_tool.rb +69 -0
- data/lib/rubino/tools/grep_tool.rb +206 -0
- data/lib/rubino/tools/memory_tool.rb +184 -0
- data/lib/rubino/tools/multi_edit_tool.rb +110 -0
- data/lib/rubino/tools/patch_tool.rb +260 -0
- data/lib/rubino/tools/probe_tool.rb +175 -0
- data/lib/rubino/tools/question_tool.rb +128 -0
- data/lib/rubino/tools/read_attachment_tool.rb +180 -0
- data/lib/rubino/tools/read_tool.rb +212 -0
- data/lib/rubino/tools/read_tracker.rb +98 -0
- data/lib/rubino/tools/registry.rb +166 -0
- data/lib/rubino/tools/result.rb +113 -0
- data/lib/rubino/tools/ruby_tool.rb +0 -0
- data/lib/rubino/tools/session_search_tool.rb +103 -0
- data/lib/rubino/tools/shell_input_tool.rb +96 -0
- data/lib/rubino/tools/shell_kill_tool.rb +76 -0
- data/lib/rubino/tools/shell_output_tool.rb +72 -0
- data/lib/rubino/tools/shell_registry.rb +158 -0
- data/lib/rubino/tools/shell_tail_tool.rb +118 -0
- data/lib/rubino/tools/shell_tool.rb +330 -0
- data/lib/rubino/tools/steer_tool.rb +118 -0
- data/lib/rubino/tools/subagent_probe.rb +89 -0
- data/lib/rubino/tools/summarize_file_tool.rb +182 -0
- data/lib/rubino/tools/task_result_tool.rb +90 -0
- data/lib/rubino/tools/task_stop_tool.rb +80 -0
- data/lib/rubino/tools/task_tool.rb +622 -0
- data/lib/rubino/tools/test_tool.rb +454 -0
- data/lib/rubino/tools/todo_tool.rb +93 -0
- data/lib/rubino/tools/tool_call_repository.rb +33 -0
- data/lib/rubino/tools/vision_tool.rb +85 -0
- data/lib/rubino/tools/webfetch_tool.rb +153 -0
- data/lib/rubino/tools/websearch_tool.rb +179 -0
- data/lib/rubino/tools/write_tool.rb +61 -0
- data/lib/rubino/trust.rb +88 -0
- data/lib/rubino/ui/api.rb +296 -0
- data/lib/rubino/ui/base.rb +252 -0
- data/lib/rubino/ui/bottom_composer.rb +1599 -0
- data/lib/rubino/ui/cli.rb +1987 -0
- data/lib/rubino/ui/completion_menu.rb +321 -0
- data/lib/rubino/ui/completion_source.rb +284 -0
- data/lib/rubino/ui/escape_reader.rb +169 -0
- data/lib/rubino/ui/indented_io.rb +88 -0
- data/lib/rubino/ui/input_history.rb +108 -0
- data/lib/rubino/ui/live_region.rb +183 -0
- data/lib/rubino/ui/markdown_renderer.rb +506 -0
- data/lib/rubino/ui/notifier.rb +163 -0
- data/lib/rubino/ui/null.rb +195 -0
- data/lib/rubino/ui/paste_store.rb +176 -0
- data/lib/rubino/ui/printer_base.rb +79 -0
- data/lib/rubino/ui/probe_wait_indicator.rb +75 -0
- data/lib/rubino/ui/queued_indicators.rb +66 -0
- data/lib/rubino/ui/status_bar.rb +100 -0
- data/lib/rubino/ui/stdout_proxy.rb +161 -0
- data/lib/rubino/ui/streaming_markdown.rb +186 -0
- data/lib/rubino/ui/subagent_cards.rb +134 -0
- data/lib/rubino/ui/subagent_view.rb +255 -0
- data/lib/rubino/ui.rb +21 -0
- data/lib/rubino/update_check.rb +187 -0
- data/lib/rubino/util/duration.rb +23 -0
- data/lib/rubino/util/hyperlink.rb +105 -0
- data/lib/rubino/util/output.rb +145 -0
- data/lib/rubino/util/secrets_mask.rb +83 -0
- data/lib/rubino/version.rb +5 -0
- data/lib/rubino/workspace.rb +85 -0
- data/lib/rubino-agent.rb +5 -0
- data/lib/rubino.rb +318 -0
- data/mise.toml +2 -0
- data/rubino-agent.gemspec +103 -0
- data/skills/ruby-expert/SKILL.md +67 -0
- data/skills/ruby-expert/references/concurrency.md +357 -0
- data/skills/ruby-expert/references/datetime-and-encoding.md +363 -0
- data/skills/ruby-expert/references/errors-and-types.md +460 -0
- data/skills/ruby-expert/references/gem-authoring.md +459 -0
- data/skills/ruby-expert/references/language-idioms.md +465 -0
- data/skills/ruby-expert/references/metaprogramming.md +339 -0
- data/skills/ruby-expert/references/oo-design.md +553 -0
- data/skills/ruby-expert/references/performance.md +383 -0
- data/skills/ruby-expert/references/rails.md +424 -0
- data/skills/ruby-expert/references/security.md +404 -0
- data/skills/ruby-expert/references/testing.md +473 -0
- data/skills/ruby-expert/references/tooling.md +466 -0
- metadata +856 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
require_relative "inline_think_filter"
|
|
7
|
+
|
|
8
|
+
module Rubino
|
|
9
|
+
module LLM
|
|
10
|
+
# Direct Bedrock runtime client using Bearer token authentication.
|
|
11
|
+
# Used when BEDROCK_API_KEY is set without BEDROCK_SECRET_KEY.
|
|
12
|
+
# Calls the Bedrock Converse API with Authorization: Bearer header.
|
|
13
|
+
# Supports tool calls via the native Bedrock Converse toolConfig format.
|
|
14
|
+
class BedrockBearerClient
|
|
15
|
+
BEDROCK_RUNTIME_HOST = "bedrock-runtime.%s.amazonaws.com"
|
|
16
|
+
|
|
17
|
+
def initialize(api_key:, region:, model_id:, show_reasoning: false, event_bus: nil)
|
|
18
|
+
@api_key = api_key
|
|
19
|
+
@region = region
|
|
20
|
+
@model_id = model_id
|
|
21
|
+
@host = BEDROCK_RUNTIME_HOST % region
|
|
22
|
+
@show_reasoning = show_reasoning
|
|
23
|
+
@event_bus = event_bus
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Sends a non-streaming chat request, returns AdapterResponse
|
|
27
|
+
def chat(messages:, tools: nil)
|
|
28
|
+
body = build_body(messages, tools: tools)
|
|
29
|
+
response = post("/model/#{URI.encode_uri_component(@model_id)}/converse", body)
|
|
30
|
+
parse_response(response)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Sends a "streaming" chat request and returns an AdapterResponse, yielding
|
|
34
|
+
# chunk HASHES shaped exactly like every other adapter:
|
|
35
|
+
# { type: :content | :thinking, text: String, message_id: Integer }
|
|
36
|
+
#
|
|
37
|
+
# Real Bedrock ConverseStream (binary eventstream) is out of scope: bearer-
|
|
38
|
+
# token auth isn't supported by ruby_llm's SigV4 Bedrock provider, and this
|
|
39
|
+
# is a plain Net::HTTP transport. We buffer the non-streaming /converse
|
|
40
|
+
# response FULLY, then replay it through InlineThinkFilter in slices so the
|
|
41
|
+
# SHAPE matches the streaming contract (typed deltas, :thinking channel,
|
|
42
|
+
# a single content block id, an explicit MESSAGE_COMPLETED boundary).
|
|
43
|
+
# Only the token cadence is synthetic.
|
|
44
|
+
#
|
|
45
|
+
# INVARIANT: we buffer the entire response BEFORE the first emit. That is
|
|
46
|
+
# what makes retrying this call (now in Agent::ModelCallRunner) safe — a
|
|
47
|
+
# transport error can only fire during post() (before any chunk reached the
|
|
48
|
+
# UI), never mid-replay, so a retry can't double output.
|
|
49
|
+
def stream(messages:, tools: nil, &block)
|
|
50
|
+
body = build_body(messages, tools: tools)
|
|
51
|
+
data = post("/model/#{URI.encode_uri_component(@model_id)}/converse", body)
|
|
52
|
+
|
|
53
|
+
# Single buffered content block ⇒ message_id is always 0. Mirrors the
|
|
54
|
+
# 2-arg emit lambda RubyLLMAdapter feeds into InlineThinkFilter.feed/flush.
|
|
55
|
+
emit = lambda do |type, text|
|
|
56
|
+
return if text.nil? || text.empty?
|
|
57
|
+
return if type == :thinking && !@show_reasoning
|
|
58
|
+
|
|
59
|
+
block&.call({ type: type, text: text, message_id: 0 })
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
think_filter = InlineThinkFilter.new
|
|
63
|
+
extract_text(data).chars.each_slice(5) do |slice|
|
|
64
|
+
think_filter.feed(slice.join, &emit)
|
|
65
|
+
end
|
|
66
|
+
think_filter.flush(&emit)
|
|
67
|
+
|
|
68
|
+
@event_bus&.emit(Interaction::Events::MESSAGE_COMPLETED, message_id: 0)
|
|
69
|
+
|
|
70
|
+
parse_response(data)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def build_body(messages, tools: nil)
|
|
76
|
+
system_msgs = messages.select { |m| role_of(m) == "system" }
|
|
77
|
+
chat_msgs = messages.reject { |m| role_of(m) == "system" }
|
|
78
|
+
|
|
79
|
+
body = {
|
|
80
|
+
messages: chat_msgs.map { |m| format_message(m) }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
body[:system] = system_msgs.map { |m| { text: content_of(m).to_s } } if system_msgs.any?
|
|
84
|
+
|
|
85
|
+
# Attach tool definitions when provided
|
|
86
|
+
if tools && !tools.empty?
|
|
87
|
+
body[:toolConfig] = {
|
|
88
|
+
tools: tools.map { |t| format_tool(t) }
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
body
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Format a message for the Bedrock Converse API.
|
|
96
|
+
# Handles plain text, assistant tool_use turns, and tool_result turns.
|
|
97
|
+
def format_message(msg)
|
|
98
|
+
role = role_of(msg)
|
|
99
|
+
content = content_of(msg)
|
|
100
|
+
tc = msg[:tool_calls] || msg["tool_calls"]
|
|
101
|
+
|
|
102
|
+
case role
|
|
103
|
+
when "assistant"
|
|
104
|
+
# Assistant message with tool use blocks
|
|
105
|
+
if tc && !tc.empty?
|
|
106
|
+
content_blocks = []
|
|
107
|
+
content_blocks << { text: content.to_s } if content && !content.to_s.empty?
|
|
108
|
+
tc.each do |call|
|
|
109
|
+
content_blocks << {
|
|
110
|
+
toolUse: {
|
|
111
|
+
toolUseId: call[:id] || call["id"],
|
|
112
|
+
name: call[:name] || call["name"],
|
|
113
|
+
input: call[:arguments] || call["arguments"] || {}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
{ role: "assistant", content: content_blocks }
|
|
118
|
+
else
|
|
119
|
+
{ role: "assistant", content: [{ text: content.to_s }] }
|
|
120
|
+
end
|
|
121
|
+
when "tool"
|
|
122
|
+
# Tool result — Bedrock expects role: "user" with toolResult content block
|
|
123
|
+
{
|
|
124
|
+
role: "user",
|
|
125
|
+
content: [{
|
|
126
|
+
toolResult: {
|
|
127
|
+
toolUseId: msg[:tool_call_id] || msg["tool_call_id"] || "unknown",
|
|
128
|
+
content: [{ text: content.to_s }]
|
|
129
|
+
}
|
|
130
|
+
}]
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
{ role: role, content: [{ text: content.to_s }] }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Format a tool definition for Bedrock toolConfig.
|
|
138
|
+
# Accepts Rubino::Tools::Base instances or plain hashes with
|
|
139
|
+
# :name/:description/:parameters keys.
|
|
140
|
+
def format_tool(tool)
|
|
141
|
+
if tool.respond_to?(:name)
|
|
142
|
+
name = tool.name
|
|
143
|
+
description = tool.description
|
|
144
|
+
schema = tool.input_schema
|
|
145
|
+
else
|
|
146
|
+
name = tool[:name] || tool["name"]
|
|
147
|
+
description = tool[:description] || tool["description"]
|
|
148
|
+
schema = tool[:parameters] || tool["parameters"] || {}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
toolSpec: {
|
|
153
|
+
name: name,
|
|
154
|
+
description: description,
|
|
155
|
+
inputSchema: { json: schema }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def post(path, body)
|
|
161
|
+
uri = URI("https://#{@host}#{path}")
|
|
162
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
163
|
+
http.use_ssl = true
|
|
164
|
+
http.read_timeout = 120
|
|
165
|
+
http.open_timeout = 30
|
|
166
|
+
|
|
167
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
168
|
+
request["Authorization"] = "Bearer #{@api_key}"
|
|
169
|
+
request["Content-Type"] = "application/json"
|
|
170
|
+
request["Accept"] = "application/json"
|
|
171
|
+
request.body = JSON.generate(body)
|
|
172
|
+
|
|
173
|
+
response = http.request(request)
|
|
174
|
+
|
|
175
|
+
unless response.code.to_i == 200
|
|
176
|
+
error_body = begin
|
|
177
|
+
JSON.parse(response.body)
|
|
178
|
+
rescue StandardError
|
|
179
|
+
{ "message" => response.body }
|
|
180
|
+
end
|
|
181
|
+
raise Rubino::Error, "Bedrock error #{response.code}: #{error_body["message"] || error_body}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
JSON.parse(response.body)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def parse_response(data)
|
|
188
|
+
text = extract_text(data)
|
|
189
|
+
tool_calls = extract_tool_calls(data)
|
|
190
|
+
|
|
191
|
+
input_tokens = data.dig("usage", "inputTokens") || 0
|
|
192
|
+
output_tokens = data.dig("usage", "outputTokens") || 0
|
|
193
|
+
|
|
194
|
+
Rubino::LLM::AdapterResponse.new(
|
|
195
|
+
content: text,
|
|
196
|
+
tool_calls: tool_calls,
|
|
197
|
+
input_tokens: input_tokens,
|
|
198
|
+
output_tokens: output_tokens,
|
|
199
|
+
model_id: @model_id
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def extract_text(data)
|
|
204
|
+
data.dig("output", "message", "content")
|
|
205
|
+
&.select { |c| c["text"] }
|
|
206
|
+
&.map { |c| c["text"] }
|
|
207
|
+
&.join("") || ""
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Extract tool use blocks from a Bedrock Converse response.
|
|
211
|
+
# Returns an array of { id:, name:, arguments: } hashes.
|
|
212
|
+
def extract_tool_calls(data)
|
|
213
|
+
content_blocks = data.dig("output", "message", "content") || []
|
|
214
|
+
content_blocks.filter_map do |block|
|
|
215
|
+
next unless block["toolUse"]
|
|
216
|
+
|
|
217
|
+
tu = block["toolUse"]
|
|
218
|
+
{
|
|
219
|
+
id: tu["toolUseId"],
|
|
220
|
+
name: tu["name"],
|
|
221
|
+
arguments: tu["input"] || {}
|
|
222
|
+
}
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def role_of(msg)
|
|
227
|
+
(msg[:role] || msg["role"]).to_s
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def content_of(msg)
|
|
231
|
+
msg[:content] || msg["content"]
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module LLM
|
|
5
|
+
# Image-path helpers shared by the vision tool, run executor, and the
|
|
6
|
+
# interaction lifecycle: extension matching, in-text image extraction,
|
|
7
|
+
# and the model-family vision heuristic.
|
|
8
|
+
class ContentBuilder
|
|
9
|
+
SUPPORTED_IMAGE_TYPES = %w[.png .jpg .jpeg .gif .webp .bmp].freeze
|
|
10
|
+
|
|
11
|
+
# True when the path has a recognised image extension. Centralised here
|
|
12
|
+
# so Executor and tools share one definition.
|
|
13
|
+
def self.image_file?(path)
|
|
14
|
+
return false if path.nil? || path.to_s.empty?
|
|
15
|
+
|
|
16
|
+
SUPPORTED_IMAGE_TYPES.include?(File.extname(path.to_s).downcase)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Detects image references in text (file paths or URLs)
|
|
20
|
+
# Extracts them and returns [cleaned_text, image_list]
|
|
21
|
+
def self.extract_images(text)
|
|
22
|
+
images = []
|
|
23
|
+
cleaned = text.dup
|
|
24
|
+
|
|
25
|
+
# Match file paths to images: /path/to/image.png or ./image.jpg
|
|
26
|
+
cleaned.gsub!(%r{(?:^|\s)((?:/|\./|~/)[^\s]+\.(?:png|jpg|jpeg|gif|webp|bmp))}i) do
|
|
27
|
+
path = ::Regexp.last_match(1).strip
|
|
28
|
+
if File.exist?(File.expand_path(path))
|
|
29
|
+
images << { type: :file, path: File.expand_path(path) }
|
|
30
|
+
"" # Remove from text
|
|
31
|
+
else
|
|
32
|
+
::Regexp.last_match(0)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Match image URLs
|
|
37
|
+
cleaned.gsub!(%r{(https?://[^\s]+\.(?:png|jpg|jpeg|gif|webp|bmp)(?:\?[^\s]*)?)}i) do
|
|
38
|
+
url = ::Regexp.last_match(1)
|
|
39
|
+
images << { type: :url, url: url }
|
|
40
|
+
"" # Remove from text
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
[cleaned.strip, images]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns true if the model_id matches a known vision-capable family.
|
|
47
|
+
# Heuristic only — Configuration#model_supports_vision? lets callers
|
|
48
|
+
# override per-tenant (e.g. behind a proxy where model_id is the literal
|
|
49
|
+
# "auto" and the real upstream is decided server-side).
|
|
50
|
+
def self.supports_vision?(model_id)
|
|
51
|
+
model_id.match?(/gpt-4|claude|gemini|minimax-m3|mimo-v|qwen.*-vl|llava/i)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module LLM
|
|
5
|
+
# Single source of truth for "does the configured model have a usable
|
|
6
|
+
# credential?" — shared by the chat boot preflight (fail-fast before the
|
|
7
|
+
# ~80s retry storm, #93), the onboarding wizard, and `doctor`.
|
|
8
|
+
#
|
|
9
|
+
# It answers two questions the same way the adapter resolves them at call
|
|
10
|
+
# time (ProviderResolver + RubyLLMAdapter#*_compatible_api_key!), so a
|
|
11
|
+
# preflight "no key" verdict matches what the model call would actually hit:
|
|
12
|
+
#
|
|
13
|
+
# * resolved_provider(config) — the concrete provider the model id /
|
|
14
|
+
# model.provider resolves to (interprets "auto").
|
|
15
|
+
# * usable?(config) — true when a key for that provider is
|
|
16
|
+
# resolvable from config (providers.<name>.api_key) or the native ENV
|
|
17
|
+
# var, false otherwise.
|
|
18
|
+
module CredentialCheck
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# The concrete provider the configured model will be routed to. Mirrors
|
|
22
|
+
# DoctorCommand#resolved_provider and the adapter's resolution: an explicit
|
|
23
|
+
# model.provider (not "auto") wins; otherwise derive from the model id.
|
|
24
|
+
def resolved_provider(config = Rubino.configuration)
|
|
25
|
+
configured = config.model_provider
|
|
26
|
+
return configured if configured && configured != "auto"
|
|
27
|
+
|
|
28
|
+
ProviderResolver.resolve(config.model_default.to_s)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# True when a credential for the resolved provider is available. The "fake"
|
|
32
|
+
# provider needs no upstream key. Honours providers.<name>.api_key first
|
|
33
|
+
# (custom / openai-compatible / anthropic-compatible gateways), then the
|
|
34
|
+
# provider's native ENV var — the same order RubyLLMAdapter uses.
|
|
35
|
+
def usable?(config = Rubino.configuration)
|
|
36
|
+
provider = resolved_provider(config)
|
|
37
|
+
return true if provider == "fake"
|
|
38
|
+
|
|
39
|
+
prov_cfg = config.provider_config(provider)
|
|
40
|
+
return true if present?(prov_cfg["api_key"])
|
|
41
|
+
return present?(ENV.fetch("OPENAI_API_KEY", nil)) if prov_cfg["openai_compatible"] == true
|
|
42
|
+
return present?(ENV.fetch("ANTHROPIC_API_KEY", nil)) if prov_cfg["anthropic_compatible"] == true
|
|
43
|
+
|
|
44
|
+
present?(provider_env_key(provider))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The native ENV credential a provider reads when no config key is set.
|
|
48
|
+
def provider_env_key(provider)
|
|
49
|
+
case provider
|
|
50
|
+
when "openai" then ENV.fetch("OPENAI_API_KEY", nil)
|
|
51
|
+
when "anthropic" then ENV.fetch("ANTHROPIC_API_KEY", nil)
|
|
52
|
+
when "google" then ENV["GEMINI_API_KEY"] || ENV.fetch("GOOGLE_API_KEY", nil)
|
|
53
|
+
when "bedrock" then ENV.fetch("BEDROCK_API_KEY", nil)
|
|
54
|
+
when "minimax" then ENV.fetch("MINIMAX_API_KEY", nil)
|
|
55
|
+
else
|
|
56
|
+
# Unknown / self-hosted provider: no native ENV mapping. Fall back to
|
|
57
|
+
# the OpenAI key, which most openai-compatible backends accept.
|
|
58
|
+
ENV.fetch("OPENAI_API_KEY", nil)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The ENV var NAME we'd suggest the user set for a given provider — used by
|
|
63
|
+
# the actionable error message and the wizard.
|
|
64
|
+
def provider_env_var_name(provider)
|
|
65
|
+
{
|
|
66
|
+
"openai" => "OPENAI_API_KEY",
|
|
67
|
+
"anthropic" => "ANTHROPIC_API_KEY",
|
|
68
|
+
"google" => "GEMINI_API_KEY",
|
|
69
|
+
"bedrock" => "BEDROCK_API_KEY",
|
|
70
|
+
"minimax" => "MINIMAX_API_KEY"
|
|
71
|
+
}.fetch(provider, "#{provider.to_s.upcase}_API_KEY")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# A clear, actionable message for an unconfigured provider/model — the
|
|
75
|
+
# text surfaced on the fail-fast path and in non-interactive contexts.
|
|
76
|
+
def missing_key_message(config = Rubino.configuration)
|
|
77
|
+
provider = resolved_provider(config)
|
|
78
|
+
env_var = provider_env_var_name(provider)
|
|
79
|
+
<<~MSG.strip
|
|
80
|
+
No API key configured for provider '#{provider}' (model #{config.model_default}).
|
|
81
|
+
Set it up one of these ways:
|
|
82
|
+
• run `rubino setup` for a guided first-run setup, or
|
|
83
|
+
• add #{env_var}=<your-key> to #{Config::Loader.new.env_path}, or
|
|
84
|
+
• set providers.#{provider}.api_key in #{Config::Loader.new.config_path}.
|
|
85
|
+
MSG
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def present?(value)
|
|
89
|
+
!value.nil? && !value.to_s.empty?
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|