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.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/.github/workflows/deploy-yard-docs.yml +52 -0
- data/CHANGELOG.md +55 -0
- data/COMMITS.md +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +332 -0
- data/Rakefile +67 -0
- data/docs/api/adapters/anthropic.md +121 -0
- data/docs/api/adapters/gemini.md +133 -0
- data/docs/api/adapters/index.md +104 -0
- data/docs/api/adapters/openai.md +134 -0
- data/docs/api/core/index.md +113 -0
- data/docs/api/core/memory.md +314 -0
- data/docs/api/core/network.md +291 -0
- data/docs/api/core/robot.md +273 -0
- data/docs/api/core/state.md +273 -0
- data/docs/api/core/tool.md +353 -0
- data/docs/api/history/active-record-adapter.md +195 -0
- data/docs/api/history/config.md +191 -0
- data/docs/api/history/index.md +132 -0
- data/docs/api/history/thread-manager.md +144 -0
- data/docs/api/index.md +82 -0
- data/docs/api/mcp/client.md +221 -0
- data/docs/api/mcp/index.md +111 -0
- data/docs/api/mcp/server.md +225 -0
- data/docs/api/mcp/transports.md +264 -0
- data/docs/api/messages/index.md +67 -0
- data/docs/api/messages/text-message.md +102 -0
- data/docs/api/messages/tool-call-message.md +144 -0
- data/docs/api/messages/tool-result-message.md +154 -0
- data/docs/api/messages/user-message.md +171 -0
- data/docs/api/streaming/context.md +174 -0
- data/docs/api/streaming/events.md +237 -0
- data/docs/api/streaming/index.md +108 -0
- data/docs/architecture/core-concepts.md +243 -0
- data/docs/architecture/index.md +138 -0
- data/docs/architecture/message-flow.md +320 -0
- data/docs/architecture/network-orchestration.md +216 -0
- data/docs/architecture/robot-execution.md +243 -0
- data/docs/architecture/state-management.md +323 -0
- data/docs/assets/css/custom.css +56 -0
- data/docs/assets/images/robot_lab.jpg +0 -0
- data/docs/concepts.md +216 -0
- data/docs/examples/basic-chat.md +193 -0
- data/docs/examples/index.md +129 -0
- data/docs/examples/mcp-server.md +290 -0
- data/docs/examples/multi-robot-network.md +312 -0
- data/docs/examples/rails-application.md +420 -0
- data/docs/examples/tool-usage.md +310 -0
- data/docs/getting-started/configuration.md +230 -0
- data/docs/getting-started/index.md +56 -0
- data/docs/getting-started/installation.md +179 -0
- data/docs/getting-started/quick-start.md +203 -0
- data/docs/guides/building-robots.md +376 -0
- data/docs/guides/creating-networks.md +366 -0
- data/docs/guides/history.md +359 -0
- data/docs/guides/index.md +68 -0
- data/docs/guides/mcp-integration.md +356 -0
- data/docs/guides/memory.md +309 -0
- data/docs/guides/rails-integration.md +432 -0
- data/docs/guides/streaming.md +314 -0
- data/docs/guides/using-tools.md +394 -0
- data/docs/index.md +160 -0
- data/examples/01_simple_robot.rb +38 -0
- data/examples/02_tools.rb +106 -0
- data/examples/03_network.rb +103 -0
- data/examples/04_mcp.rb +219 -0
- data/examples/05_streaming.rb +124 -0
- data/examples/06_prompt_templates.rb +324 -0
- data/examples/07_network_memory.rb +329 -0
- data/examples/prompts/assistant/system.txt.erb +2 -0
- data/examples/prompts/assistant/user.txt.erb +1 -0
- data/examples/prompts/billing/system.txt.erb +7 -0
- data/examples/prompts/billing/user.txt.erb +1 -0
- data/examples/prompts/classifier/system.txt.erb +4 -0
- data/examples/prompts/classifier/user.txt.erb +1 -0
- data/examples/prompts/entity_extractor/system.txt.erb +11 -0
- data/examples/prompts/entity_extractor/user.txt.erb +3 -0
- data/examples/prompts/escalation/system.txt.erb +35 -0
- data/examples/prompts/escalation/user.txt.erb +34 -0
- data/examples/prompts/general/system.txt.erb +4 -0
- data/examples/prompts/general/user.txt.erb +1 -0
- data/examples/prompts/github_assistant/system.txt.erb +6 -0
- data/examples/prompts/github_assistant/user.txt.erb +1 -0
- data/examples/prompts/helper/system.txt.erb +1 -0
- data/examples/prompts/helper/user.txt.erb +1 -0
- data/examples/prompts/keyword_extractor/system.txt.erb +8 -0
- data/examples/prompts/keyword_extractor/user.txt.erb +3 -0
- data/examples/prompts/order_support/system.txt.erb +27 -0
- data/examples/prompts/order_support/user.txt.erb +22 -0
- data/examples/prompts/product_support/system.txt.erb +30 -0
- data/examples/prompts/product_support/user.txt.erb +32 -0
- data/examples/prompts/sentiment_analyzer/system.txt.erb +9 -0
- data/examples/prompts/sentiment_analyzer/user.txt.erb +3 -0
- data/examples/prompts/synthesizer/system.txt.erb +14 -0
- data/examples/prompts/synthesizer/user.txt.erb +15 -0
- data/examples/prompts/technical/system.txt.erb +7 -0
- data/examples/prompts/technical/user.txt.erb +1 -0
- data/examples/prompts/triage/system.txt.erb +16 -0
- data/examples/prompts/triage/user.txt.erb +17 -0
- data/lib/generators/robot_lab/install_generator.rb +78 -0
- data/lib/generators/robot_lab/robot_generator.rb +55 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +41 -0
- data/lib/generators/robot_lab/templates/migration.rb.tt +32 -0
- data/lib/generators/robot_lab/templates/result_model.rb.tt +52 -0
- data/lib/generators/robot_lab/templates/robot.rb.tt +46 -0
- data/lib/generators/robot_lab/templates/robot_test.rb.tt +32 -0
- data/lib/generators/robot_lab/templates/routing_robot.rb.tt +53 -0
- data/lib/generators/robot_lab/templates/thread_model.rb.tt +40 -0
- data/lib/robot_lab/adapters/anthropic.rb +163 -0
- data/lib/robot_lab/adapters/base.rb +85 -0
- data/lib/robot_lab/adapters/gemini.rb +193 -0
- data/lib/robot_lab/adapters/openai.rb +159 -0
- data/lib/robot_lab/adapters/registry.rb +81 -0
- data/lib/robot_lab/configuration.rb +143 -0
- data/lib/robot_lab/error.rb +32 -0
- data/lib/robot_lab/errors.rb +70 -0
- data/lib/robot_lab/history/active_record_adapter.rb +146 -0
- data/lib/robot_lab/history/config.rb +115 -0
- data/lib/robot_lab/history/thread_manager.rb +93 -0
- data/lib/robot_lab/mcp/client.rb +210 -0
- data/lib/robot_lab/mcp/server.rb +84 -0
- data/lib/robot_lab/mcp/transports/base.rb +56 -0
- data/lib/robot_lab/mcp/transports/sse.rb +117 -0
- data/lib/robot_lab/mcp/transports/stdio.rb +133 -0
- data/lib/robot_lab/mcp/transports/streamable_http.rb +139 -0
- data/lib/robot_lab/mcp/transports/websocket.rb +108 -0
- data/lib/robot_lab/memory.rb +882 -0
- data/lib/robot_lab/memory_change.rb +123 -0
- data/lib/robot_lab/message.rb +357 -0
- data/lib/robot_lab/network.rb +350 -0
- data/lib/robot_lab/rails/engine.rb +29 -0
- data/lib/robot_lab/rails/railtie.rb +42 -0
- data/lib/robot_lab/robot.rb +560 -0
- data/lib/robot_lab/robot_result.rb +205 -0
- data/lib/robot_lab/robotic_model.rb +324 -0
- data/lib/robot_lab/state_proxy.rb +188 -0
- data/lib/robot_lab/streaming/context.rb +144 -0
- data/lib/robot_lab/streaming/events.rb +95 -0
- data/lib/robot_lab/streaming/sequence_counter.rb +48 -0
- data/lib/robot_lab/task.rb +117 -0
- data/lib/robot_lab/tool.rb +223 -0
- data/lib/robot_lab/tool_config.rb +112 -0
- data/lib/robot_lab/tool_manifest.rb +234 -0
- data/lib/robot_lab/user_message.rb +118 -0
- data/lib/robot_lab/version.rb +5 -0
- data/lib/robot_lab/waiter.rb +73 -0
- data/lib/robot_lab.rb +195 -0
- data/mkdocs.yml +214 -0
- data/sig/robot_lab.rbs +4 -0
- 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
|