robot_lab 0.0.1

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.
Files changed (153) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +52 -0
  4. data/.github/workflows/deploy-yard-docs.yml +52 -0
  5. data/CHANGELOG.md +55 -0
  6. data/COMMITS.md +196 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +332 -0
  9. data/Rakefile +67 -0
  10. data/docs/api/adapters/anthropic.md +121 -0
  11. data/docs/api/adapters/gemini.md +133 -0
  12. data/docs/api/adapters/index.md +104 -0
  13. data/docs/api/adapters/openai.md +134 -0
  14. data/docs/api/core/index.md +113 -0
  15. data/docs/api/core/memory.md +314 -0
  16. data/docs/api/core/network.md +291 -0
  17. data/docs/api/core/robot.md +273 -0
  18. data/docs/api/core/state.md +273 -0
  19. data/docs/api/core/tool.md +353 -0
  20. data/docs/api/history/active-record-adapter.md +195 -0
  21. data/docs/api/history/config.md +191 -0
  22. data/docs/api/history/index.md +132 -0
  23. data/docs/api/history/thread-manager.md +144 -0
  24. data/docs/api/index.md +82 -0
  25. data/docs/api/mcp/client.md +221 -0
  26. data/docs/api/mcp/index.md +111 -0
  27. data/docs/api/mcp/server.md +225 -0
  28. data/docs/api/mcp/transports.md +264 -0
  29. data/docs/api/messages/index.md +67 -0
  30. data/docs/api/messages/text-message.md +102 -0
  31. data/docs/api/messages/tool-call-message.md +144 -0
  32. data/docs/api/messages/tool-result-message.md +154 -0
  33. data/docs/api/messages/user-message.md +171 -0
  34. data/docs/api/streaming/context.md +174 -0
  35. data/docs/api/streaming/events.md +237 -0
  36. data/docs/api/streaming/index.md +108 -0
  37. data/docs/architecture/core-concepts.md +243 -0
  38. data/docs/architecture/index.md +138 -0
  39. data/docs/architecture/message-flow.md +320 -0
  40. data/docs/architecture/network-orchestration.md +216 -0
  41. data/docs/architecture/robot-execution.md +243 -0
  42. data/docs/architecture/state-management.md +323 -0
  43. data/docs/assets/css/custom.css +56 -0
  44. data/docs/assets/images/robot_lab.jpg +0 -0
  45. data/docs/concepts.md +216 -0
  46. data/docs/examples/basic-chat.md +193 -0
  47. data/docs/examples/index.md +129 -0
  48. data/docs/examples/mcp-server.md +290 -0
  49. data/docs/examples/multi-robot-network.md +312 -0
  50. data/docs/examples/rails-application.md +420 -0
  51. data/docs/examples/tool-usage.md +310 -0
  52. data/docs/getting-started/configuration.md +230 -0
  53. data/docs/getting-started/index.md +56 -0
  54. data/docs/getting-started/installation.md +179 -0
  55. data/docs/getting-started/quick-start.md +203 -0
  56. data/docs/guides/building-robots.md +376 -0
  57. data/docs/guides/creating-networks.md +366 -0
  58. data/docs/guides/history.md +359 -0
  59. data/docs/guides/index.md +68 -0
  60. data/docs/guides/mcp-integration.md +356 -0
  61. data/docs/guides/memory.md +309 -0
  62. data/docs/guides/rails-integration.md +432 -0
  63. data/docs/guides/streaming.md +314 -0
  64. data/docs/guides/using-tools.md +394 -0
  65. data/docs/index.md +160 -0
  66. data/examples/01_simple_robot.rb +38 -0
  67. data/examples/02_tools.rb +106 -0
  68. data/examples/03_network.rb +103 -0
  69. data/examples/04_mcp.rb +219 -0
  70. data/examples/05_streaming.rb +124 -0
  71. data/examples/06_prompt_templates.rb +324 -0
  72. data/examples/07_network_memory.rb +329 -0
  73. data/examples/prompts/assistant/system.txt.erb +2 -0
  74. data/examples/prompts/assistant/user.txt.erb +1 -0
  75. data/examples/prompts/billing/system.txt.erb +7 -0
  76. data/examples/prompts/billing/user.txt.erb +1 -0
  77. data/examples/prompts/classifier/system.txt.erb +4 -0
  78. data/examples/prompts/classifier/user.txt.erb +1 -0
  79. data/examples/prompts/entity_extractor/system.txt.erb +11 -0
  80. data/examples/prompts/entity_extractor/user.txt.erb +3 -0
  81. data/examples/prompts/escalation/system.txt.erb +35 -0
  82. data/examples/prompts/escalation/user.txt.erb +34 -0
  83. data/examples/prompts/general/system.txt.erb +4 -0
  84. data/examples/prompts/general/user.txt.erb +1 -0
  85. data/examples/prompts/github_assistant/system.txt.erb +6 -0
  86. data/examples/prompts/github_assistant/user.txt.erb +1 -0
  87. data/examples/prompts/helper/system.txt.erb +1 -0
  88. data/examples/prompts/helper/user.txt.erb +1 -0
  89. data/examples/prompts/keyword_extractor/system.txt.erb +8 -0
  90. data/examples/prompts/keyword_extractor/user.txt.erb +3 -0
  91. data/examples/prompts/order_support/system.txt.erb +27 -0
  92. data/examples/prompts/order_support/user.txt.erb +22 -0
  93. data/examples/prompts/product_support/system.txt.erb +30 -0
  94. data/examples/prompts/product_support/user.txt.erb +32 -0
  95. data/examples/prompts/sentiment_analyzer/system.txt.erb +9 -0
  96. data/examples/prompts/sentiment_analyzer/user.txt.erb +3 -0
  97. data/examples/prompts/synthesizer/system.txt.erb +14 -0
  98. data/examples/prompts/synthesizer/user.txt.erb +15 -0
  99. data/examples/prompts/technical/system.txt.erb +7 -0
  100. data/examples/prompts/technical/user.txt.erb +1 -0
  101. data/examples/prompts/triage/system.txt.erb +16 -0
  102. data/examples/prompts/triage/user.txt.erb +17 -0
  103. data/lib/generators/robot_lab/install_generator.rb +78 -0
  104. data/lib/generators/robot_lab/robot_generator.rb +55 -0
  105. data/lib/generators/robot_lab/templates/initializer.rb.tt +41 -0
  106. data/lib/generators/robot_lab/templates/migration.rb.tt +32 -0
  107. data/lib/generators/robot_lab/templates/result_model.rb.tt +52 -0
  108. data/lib/generators/robot_lab/templates/robot.rb.tt +46 -0
  109. data/lib/generators/robot_lab/templates/robot_test.rb.tt +32 -0
  110. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +53 -0
  111. data/lib/generators/robot_lab/templates/thread_model.rb.tt +40 -0
  112. data/lib/robot_lab/adapters/anthropic.rb +163 -0
  113. data/lib/robot_lab/adapters/base.rb +85 -0
  114. data/lib/robot_lab/adapters/gemini.rb +193 -0
  115. data/lib/robot_lab/adapters/openai.rb +159 -0
  116. data/lib/robot_lab/adapters/registry.rb +81 -0
  117. data/lib/robot_lab/configuration.rb +143 -0
  118. data/lib/robot_lab/error.rb +32 -0
  119. data/lib/robot_lab/errors.rb +70 -0
  120. data/lib/robot_lab/history/active_record_adapter.rb +146 -0
  121. data/lib/robot_lab/history/config.rb +115 -0
  122. data/lib/robot_lab/history/thread_manager.rb +93 -0
  123. data/lib/robot_lab/mcp/client.rb +210 -0
  124. data/lib/robot_lab/mcp/server.rb +84 -0
  125. data/lib/robot_lab/mcp/transports/base.rb +56 -0
  126. data/lib/robot_lab/mcp/transports/sse.rb +117 -0
  127. data/lib/robot_lab/mcp/transports/stdio.rb +133 -0
  128. data/lib/robot_lab/mcp/transports/streamable_http.rb +139 -0
  129. data/lib/robot_lab/mcp/transports/websocket.rb +108 -0
  130. data/lib/robot_lab/memory.rb +882 -0
  131. data/lib/robot_lab/memory_change.rb +123 -0
  132. data/lib/robot_lab/message.rb +357 -0
  133. data/lib/robot_lab/network.rb +350 -0
  134. data/lib/robot_lab/rails/engine.rb +29 -0
  135. data/lib/robot_lab/rails/railtie.rb +42 -0
  136. data/lib/robot_lab/robot.rb +560 -0
  137. data/lib/robot_lab/robot_result.rb +205 -0
  138. data/lib/robot_lab/robotic_model.rb +324 -0
  139. data/lib/robot_lab/state_proxy.rb +188 -0
  140. data/lib/robot_lab/streaming/context.rb +144 -0
  141. data/lib/robot_lab/streaming/events.rb +95 -0
  142. data/lib/robot_lab/streaming/sequence_counter.rb +48 -0
  143. data/lib/robot_lab/task.rb +117 -0
  144. data/lib/robot_lab/tool.rb +223 -0
  145. data/lib/robot_lab/tool_config.rb +112 -0
  146. data/lib/robot_lab/tool_manifest.rb +234 -0
  147. data/lib/robot_lab/user_message.rb +118 -0
  148. data/lib/robot_lab/version.rb +5 -0
  149. data/lib/robot_lab/waiter.rb +73 -0
  150. data/lib/robot_lab.rb +195 -0
  151. data/mkdocs.yml +214 -0
  152. data/sig/robot_lab.rbs +4 -0
  153. metadata +442 -0
@@ -0,0 +1,16 @@
1
+ You are an intelligent customer support triage specialist for <%= company_name %>.
2
+
3
+ Your job is to analyze incoming customer requests and classify them into the appropriate category so they can be routed to the right specialist.
4
+
5
+ ## Available Categories
6
+ <% categories.each do |category| %>
7
+ - **<%= category[:name] %>**: <%= category[:description] %>
8
+ <% end %>
9
+
10
+ ## Classification Rules
11
+ 1. Analyze the customer's message carefully
12
+ 2. Consider their account history and status
13
+ 3. Choose the SINGLE most appropriate category
14
+ 4. If unclear, default to "escalation" for human review
15
+
16
+ Respond with ONLY the category name in lowercase, nothing else.
@@ -0,0 +1,17 @@
1
+ Customer Message: <%= message %>
2
+
3
+ <% if customer && customer[:recent_orders]&.any? %>
4
+ Recent Order Activity:
5
+ <% customer[:recent_orders].each do |order| %>
6
+ - Order #<%= order[:id] %> (<%= order[:date] %>): <%= order[:status] %> - $<%= order[:total] %>
7
+ <% end %>
8
+ <% end %>
9
+
10
+ <% if customer && customer[:open_tickets]&.any? %>
11
+ Open Support Tickets:
12
+ <% customer[:open_tickets].each do |ticket| %>
13
+ - Ticket #<%= ticket[:id] %>: <%= ticket[:subject] %> (<%= ticket[:status] %>)
14
+ <% end %>
15
+ <% end %>
16
+
17
+ Please classify this request.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module RobotLab
7
+ module Generators
8
+ # Installs RobotLab into a Rails application
9
+ #
10
+ # Usage:
11
+ # rails generate robot_lab:install
12
+ #
13
+ class InstallGenerator < ::Rails::Generators::Base
14
+ include ::Rails::Generators::Migration
15
+
16
+ source_root File.expand_path("templates", __dir__)
17
+
18
+ class_option :skip_migration, type: :boolean, default: false,
19
+ desc: "Skip database migration generation"
20
+
21
+ # Returns the next migration number for ActiveRecord migrations.
22
+ #
23
+ # @param dirname [String] the migrations directory
24
+ # @return [String] the next migration number
25
+ def self.next_migration_number(dirname)
26
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
27
+ end
28
+
29
+ # Creates the RobotLab initializer file.
30
+ #
31
+ # @return [void]
32
+ def create_initializer
33
+ template "initializer.rb.tt", "config/initializers/robot_lab.rb"
34
+ end
35
+
36
+ # Creates the database migration for RobotLab tables.
37
+ #
38
+ # @return [void]
39
+ def create_migration
40
+ return if options[:skip_migration]
41
+
42
+ migration_template "migration.rb.tt", "db/migrate/create_robot_lab_tables.rb"
43
+ end
44
+
45
+ # Creates the ActiveRecord model files.
46
+ #
47
+ # @return [void]
48
+ def create_models
49
+ return if options[:skip_migration]
50
+
51
+ template "thread_model.rb.tt", "app/models/robot_lab_thread.rb"
52
+ template "result_model.rb.tt", "app/models/robot_lab_result.rb"
53
+ end
54
+
55
+ # Creates the robots and tools directories.
56
+ #
57
+ # @return [void]
58
+ def create_directories
59
+ empty_directory "app/robots"
60
+ empty_directory "app/tools"
61
+ end
62
+
63
+ # Displays post-installation instructions.
64
+ #
65
+ # @return [void]
66
+ def display_post_install
67
+ say ""
68
+ say "RobotLab installed successfully!", :green
69
+ say ""
70
+ say "Next steps:"
71
+ say " 1. Run migrations: rails db:migrate"
72
+ say " 2. Configure your LLM API keys in config/initializers/robot_lab.rb"
73
+ say " 3. Generate your first robot: rails g robot_lab:robot MyRobot"
74
+ say ""
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module RobotLab
6
+ module Generators
7
+ # Generates a new RobotLab robot
8
+ #
9
+ # Usage:
10
+ # rails generate robot_lab:robot NAME [options]
11
+ #
12
+ # Examples:
13
+ # rails generate robot_lab:robot Support
14
+ # rails generate robot_lab:robot Billing --description="Handles billing queries"
15
+ #
16
+ class RobotGenerator < ::Rails::Generators::NamedBase
17
+ source_root File.expand_path("templates", __dir__)
18
+
19
+ class_option :description, type: :string, default: nil,
20
+ desc: "Robot description"
21
+ class_option :routing, type: :boolean, default: false,
22
+ desc: "Generate a routing robot"
23
+ class_option :tools, type: :array, default: [],
24
+ desc: "List of tools to include"
25
+
26
+ # Creates the robot class file.
27
+ #
28
+ # @return [void]
29
+ def create_robot_file
30
+ if options[:routing]
31
+ template "routing_robot.rb.tt", "app/robots/#{file_name}_robot.rb"
32
+ else
33
+ template "robot.rb.tt", "app/robots/#{file_name}_robot.rb"
34
+ end
35
+ end
36
+
37
+ # Creates the robot test file.
38
+ #
39
+ # @return [void]
40
+ def create_test_file
41
+ template "robot_test.rb.tt", "test/robots/#{file_name}_robot_test.rb"
42
+ end
43
+
44
+ private
45
+
46
+ def robot_description
47
+ options[:description] || "A helpful #{class_name.titleize} robot"
48
+ end
49
+
50
+ def robot_tools
51
+ options[:tools]
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RobotLab Configuration
4
+ #
5
+ # Configure your AI robot framework settings here.
6
+ #
7
+ RobotLab.configure do |config|
8
+ # Default model for robots (optional)
9
+ # config.default_model = "claude-sonnet-4"
10
+ # config.default_provider = :anthropic
11
+
12
+ # Logger configuration
13
+ config.logger = Rails.logger
14
+
15
+ # Maximum iterations per robot run
16
+ # config.max_iterations = 10
17
+
18
+ # Enable debug mode in development
19
+ # config.debug = Rails.env.development?
20
+ end
21
+
22
+ # Configure Ruby LLM (required for LLM interactions)
23
+ #
24
+ # RubyLLM.configure do |config|
25
+ # config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
26
+ # config.openai_api_key = ENV["OPENAI_API_KEY"]
27
+ # config.gemini_api_key = ENV["GEMINI_API_KEY"]
28
+ # end
29
+
30
+ # History Persistence (optional)
31
+ #
32
+ # Uncomment to enable conversation history storage:
33
+ #
34
+ # Rails.application.config.after_initialize do
35
+ # adapter = RobotLab::History::ActiveRecordAdapter.new(
36
+ # thread_model: RobotLabThread,
37
+ # result_model: RobotLabResult
38
+ # )
39
+ #
40
+ # RobotLab.configuration.history = adapter.to_config
41
+ # end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRobotLabTables < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :robot_lab_threads do |t|
6
+ t.string :session_id, null: false, index: { unique: true }
7
+ t.text :initial_input
8
+ t.json :input_metadata, default: {}
9
+ t.json :state_data, default: {}
10
+ t.text :last_user_message
11
+ t.datetime :last_user_message_at
12
+
13
+ t.timestamps
14
+ end
15
+
16
+ create_table :robot_lab_results do |t|
17
+ t.string :session_id, null: false, index: true
18
+ t.string :robot_name, null: false
19
+ t.integer :sequence_number, null: false, default: 0
20
+ t.json :output_data, default: []
21
+ t.json :tool_calls_data, default: []
22
+ t.string :stop_reason
23
+ t.string :checksum
24
+
25
+ t.timestamps
26
+ end
27
+
28
+ add_index :robot_lab_results, [:session_id, :sequence_number]
29
+ add_foreign_key :robot_lab_results, :robot_lab_threads,
30
+ column: :session_id, primary_key: :session_id
31
+ end
32
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RobotLab Result Model
4
+ #
5
+ # Stores robot execution results for history persistence.
6
+ #
7
+ class RobotLabResult < ApplicationRecord
8
+ belongs_to :thread,
9
+ class_name: "RobotLabThread",
10
+ foreign_key: :session_id,
11
+ primary_key: :session_id
12
+
13
+ validates :session_id, presence: true
14
+ validates :robot_name, presence: true
15
+ validates :sequence_number, presence: true,
16
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
17
+
18
+ default_scope { order(sequence_number: :asc) }
19
+
20
+ # Get output messages as RobotLab::Message objects
21
+ #
22
+ # @return [Array<RobotLab::Message>]
23
+ #
24
+ def output_messages
25
+ (output_data || []).map do |data|
26
+ RobotLab::Message.from_hash(data.symbolize_keys)
27
+ end
28
+ end
29
+
30
+ # Get tool calls as RobotLab::Message objects
31
+ #
32
+ # @return [Array<RobotLab::Message>]
33
+ #
34
+ def tool_call_messages
35
+ (tool_calls_data || []).map do |data|
36
+ RobotLab::Message.from_hash(data.symbolize_keys)
37
+ end
38
+ end
39
+
40
+ # Convert to RobotLab::RobotResult
41
+ #
42
+ # @return [RobotLab::RobotResult]
43
+ #
44
+ def to_robot_result
45
+ RobotLab::RobotResult.new(
46
+ robot_name: robot_name,
47
+ output: output_messages,
48
+ tool_calls: tool_call_messages,
49
+ stop_reason: stop_reason
50
+ )
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # <%= class_name %> Robot
4
+ #
5
+ # <%= robot_description %>
6
+ #
7
+ class <%= class_name %>Robot
8
+ include RobotLab::Lifecycle
9
+
10
+ SYSTEM_PROMPT = <<~PROMPT
11
+ You are a helpful <%= class_name.titleize %> assistant.
12
+
13
+ Your role is to assist users with their requests in a friendly and efficient manner.
14
+ PROMPT
15
+
16
+ def self.build
17
+ RobotLab.create_robot(
18
+ name: "<%= file_name %>",
19
+ description: "<%= robot_description %>",
20
+ system: SYSTEM_PROMPT,
21
+ tools: tools,
22
+ lifecycle: lifecycle_hooks
23
+ )
24
+ end
25
+
26
+ def self.tools
27
+ [
28
+ <%- robot_tools.each do |tool| -%>
29
+ # <%= tool.camelize %>Tool.build,
30
+ <%- end -%>
31
+ ]
32
+ end
33
+
34
+ def self.lifecycle_hooks
35
+ RobotLab::Lifecycle::Hooks.new(
36
+ on_start: ->(robot:, network:, prompt:, history:) {
37
+ # Called before robot execution
38
+ { prompt: prompt, history: history }
39
+ },
40
+ on_finish: ->(robot:, network:, result:) {
41
+ # Called after robot execution
42
+ result
43
+ }
44
+ )
45
+ end
46
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class <%= class_name %>RobotTest < ActiveSupport::TestCase
6
+ def setup
7
+ @robot = <%= class_name %>Robot.build
8
+ end
9
+
10
+ test "robot has correct name" do
11
+ assert_equal "<%= file_name %>", @robot.name
12
+ end
13
+
14
+ test "robot has description" do
15
+ assert_not_nil @robot.description
16
+ end
17
+
18
+ test "robot has system prompt" do
19
+ assert_not_nil @robot.instance_variable_get(:@system)
20
+ end
21
+
22
+ # Add more tests as needed:
23
+ #
24
+ # test "robot runs successfully" do
25
+ # result = @robot.run("Hello, I need help")
26
+ # assert result.is_a?(RobotLab::RobotResult)
27
+ # end
28
+ #
29
+ # test "robot uses correct tools" do
30
+ # # Test tool invocation
31
+ # end
32
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # <%= class_name %> Routing Robot
4
+ #
5
+ # <%= robot_description %>
6
+ #
7
+ # This robot routes requests to other robots based on classification.
8
+ #
9
+ class <%= class_name %>Robot
10
+ include RobotLab::Lifecycle
11
+
12
+ SYSTEM_PROMPT = <<~PROMPT
13
+ You are a routing robot that classifies user requests and directs them
14
+ to the appropriate specialist robot.
15
+
16
+ Analyze the user's request and determine which robot should handle it.
17
+ Respond with your classification decision.
18
+ PROMPT
19
+
20
+ def self.build
21
+ RobotLab.create_routing_robot(
22
+ name: "<%= file_name %>",
23
+ description: "<%= robot_description %>",
24
+ system: SYSTEM_PROMPT,
25
+ on_route: method(:route_decision)
26
+ )
27
+ end
28
+
29
+ # Determine which robot should handle the request
30
+ #
31
+ # @param result [RobotLab::RobotResult] Classification result
32
+ # @param robot [RobotLab::Robot] This robot
33
+ # @param network [RobotLab::Network] Parent network
34
+ # @return [Array<String>, nil] Robot names to route to
35
+ #
36
+ def self.route_decision(result:, robot:, network:)
37
+ # Extract classification from the result
38
+ output = result.output.last&.content.to_s.downcase
39
+
40
+ # Route based on classification
41
+ # Example routing logic:
42
+ # case output
43
+ # when /billing|payment|invoice/
44
+ # ["billing_robot"]
45
+ # when /technical|bug|error/
46
+ # ["technical_robot"]
47
+ # else
48
+ # ["general_robot"]
49
+ # end
50
+
51
+ nil # Return nil to end the network run
52
+ end
53
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RobotLab Thread Model
4
+ #
5
+ # Stores conversation threads for history persistence.
6
+ #
7
+ class RobotLabThread < ApplicationRecord
8
+ has_many :results,
9
+ class_name: "RobotLabResult",
10
+ foreign_key: :session_id,
11
+ primary_key: :session_id,
12
+ dependent: :destroy
13
+
14
+ validates :session_id, presence: true, uniqueness: true
15
+
16
+ # Find or create a thread by ID
17
+ #
18
+ # @param id [String] Thread ID
19
+ # @return [RobotLabThread]
20
+ #
21
+ def self.find_or_create_by_session_id(id)
22
+ find_or_create_by(session_id: id)
23
+ end
24
+
25
+ # Get the last result for this thread
26
+ #
27
+ # @return [RobotLabResult, nil]
28
+ #
29
+ def last_result
30
+ results.order(sequence_number: :desc).first
31
+ end
32
+
33
+ # Get message count for this thread
34
+ #
35
+ # @return [Integer]
36
+ #
37
+ def message_count
38
+ results.count
39
+ end
40
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ module Adapters
5
+ # Adapter for Anthropic Claude models
6
+ #
7
+ # Handles Anthropic-specific API conventions:
8
+ # - System message as top-level parameter (not in messages array)
9
+ # - Tool use/result format differences
10
+ # - Content block structure
11
+ #
12
+ class Anthropic < Base
13
+ # Creates a new Anthropic adapter instance.
14
+ def initialize
15
+ super(:anthropic)
16
+ end
17
+
18
+ # Format messages for Anthropic API
19
+ #
20
+ # Anthropic requires system message at top level, not in messages array.
21
+ # Also handles tool_use and tool_result message formats.
22
+ #
23
+ # @param messages [Array<Message>]
24
+ # @return [Array<Hash>]
25
+ #
26
+ def format_messages(messages)
27
+ conversation_messages(messages).map do |msg|
28
+ format_single_message(msg)
29
+ end
30
+ end
31
+
32
+ # Parse Anthropic response into internal messages
33
+ #
34
+ # @param response [RubyLLM::Response] ruby_llm response object
35
+ # @return [Array<Message>]
36
+ #
37
+ def parse_response(response)
38
+ messages = []
39
+
40
+ # Handle text content
41
+ if response.content && !response.content.empty?
42
+ messages << TextMessage.new(
43
+ role: "assistant",
44
+ content: response.content,
45
+ stop_reason: response.tool_calls&.any? ? "tool" : "stop"
46
+ )
47
+ end
48
+
49
+ # Handle tool calls
50
+ if response.tool_calls&.any?
51
+ tool_messages = response.tool_calls.map do |id, tool_call|
52
+ ToolMessage.new(
53
+ id: id,
54
+ name: tool_call.name,
55
+ input: parse_tool_arguments(tool_call.arguments)
56
+ )
57
+ end
58
+
59
+ messages << ToolCallMessage.new(
60
+ role: "assistant",
61
+ tools: tool_messages,
62
+ stop_reason: "tool"
63
+ )
64
+ end
65
+
66
+ messages
67
+ end
68
+
69
+ # Format tools for Anthropic
70
+ #
71
+ # @param tools [Array<Tool>]
72
+ # @return [Array<Hash>]
73
+ #
74
+ def format_tools(tools)
75
+ tools.map do |tool|
76
+ schema = tool.to_json_schema
77
+ {
78
+ name: schema[:name],
79
+ description: schema[:description],
80
+ input_schema: schema[:parameters] || { type: "object", properties: {} }
81
+ }
82
+ end
83
+ end
84
+
85
+ # Anthropic tool choice format
86
+ #
87
+ # @param choice [String, Symbol]
88
+ # @return [Hash]
89
+ #
90
+ def format_tool_choice(choice)
91
+ case choice.to_s
92
+ when "auto" then { type: "auto" }
93
+ when "any" then { type: "any" }
94
+ else { type: "tool", name: choice.to_s }
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def format_single_message(msg)
101
+ case msg
102
+ when TextMessage
103
+ { role: msg.role, content: msg.content }
104
+ when ToolCallMessage
105
+ {
106
+ role: "assistant",
107
+ content: msg.tools.map do |tool|
108
+ {
109
+ type: "tool_use",
110
+ id: tool.id,
111
+ name: tool.name,
112
+ input: tool.input
113
+ }
114
+ end
115
+ }
116
+ when ToolResultMessage
117
+ {
118
+ role: "user",
119
+ content: [
120
+ {
121
+ type: "tool_result",
122
+ tool_use_id: msg.tool.id,
123
+ content: format_tool_result_content(msg.content)
124
+ }
125
+ ]
126
+ }
127
+ else
128
+ { role: msg.role, content: msg.content.to_s }
129
+ end
130
+ end
131
+
132
+ def format_tool_result_content(content)
133
+ case content
134
+ when Hash
135
+ if content[:error]
136
+ JSON.generate(content)
137
+ elsif content[:data]
138
+ content[:data].is_a?(String) ? content[:data] : JSON.generate(content[:data])
139
+ else
140
+ JSON.generate(content)
141
+ end
142
+ else
143
+ content.to_s
144
+ end
145
+ end
146
+
147
+ def parse_tool_arguments(arguments)
148
+ case arguments
149
+ when String
150
+ begin
151
+ JSON.parse(arguments, symbolize_names: true)
152
+ rescue JSON::ParserError
153
+ { raw: arguments }
154
+ end
155
+ when Hash
156
+ arguments.transform_keys(&:to_sym)
157
+ else
158
+ arguments || {}
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end