robot_lab 0.0.1 → 0.0.6
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/.github/workflows/deploy-github-pages.yml +9 -9
- data/.irbrc +6 -0
- data/CHANGELOG.md +140 -0
- data/README.md +263 -48
- data/Rakefile +71 -1
- data/docs/api/core/index.md +53 -46
- data/docs/api/core/memory.md +200 -154
- data/docs/api/core/network.md +13 -3
- data/docs/api/core/robot.md +490 -130
- data/docs/api/core/state.md +55 -73
- data/docs/api/core/tool.md +205 -209
- data/docs/api/index.md +7 -28
- data/docs/api/mcp/client.md +119 -48
- data/docs/api/mcp/index.md +75 -60
- data/docs/api/mcp/server.md +120 -136
- data/docs/api/mcp/transports.md +172 -184
- data/docs/api/messages/index.md +35 -20
- data/docs/api/messages/text-message.md +67 -21
- data/docs/api/messages/tool-call-message.md +80 -41
- data/docs/api/messages/tool-result-message.md +119 -50
- data/docs/api/messages/user-message.md +48 -24
- data/docs/api/streaming/context.md +157 -74
- data/docs/api/streaming/events.md +114 -166
- data/docs/api/streaming/index.md +74 -72
- data/docs/architecture/core-concepts.md +360 -116
- data/docs/architecture/index.md +97 -59
- data/docs/architecture/message-flow.md +138 -129
- data/docs/architecture/network-orchestration.md +197 -50
- data/docs/architecture/robot-execution.md +199 -146
- data/docs/architecture/state-management.md +255 -187
- data/docs/concepts.md +311 -49
- data/docs/examples/basic-chat.md +89 -77
- data/docs/examples/index.md +222 -47
- data/docs/examples/mcp-server.md +207 -203
- data/docs/examples/multi-robot-network.md +129 -35
- data/docs/examples/rails-application.md +159 -160
- data/docs/examples/tool-usage.md +295 -204
- data/docs/getting-started/configuration.md +347 -154
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/installation.md +22 -13
- data/docs/getting-started/quick-start.md +166 -121
- data/docs/guides/building-robots.md +418 -212
- data/docs/guides/creating-networks.md +143 -24
- data/docs/guides/index.md +0 -5
- data/docs/guides/mcp-integration.md +152 -113
- data/docs/guides/memory.md +220 -164
- data/docs/guides/rails-integration.md +244 -162
- data/docs/guides/streaming.md +137 -187
- data/docs/guides/using-tools.md +259 -212
- data/docs/index.md +46 -41
- data/examples/01_simple_robot.rb +6 -9
- data/examples/02_tools.rb +6 -9
- data/examples/03_network.rb +19 -17
- data/examples/04_mcp.rb +5 -8
- data/examples/05_streaming.rb +5 -8
- data/examples/06_prompt_templates.rb +42 -37
- data/examples/07_network_memory.rb +13 -14
- data/examples/08_llm_config.rb +169 -0
- data/examples/09_chaining.rb +262 -0
- data/examples/10_memory.rb +331 -0
- data/examples/11_network_introspection.rb +253 -0
- data/examples/12_message_bus.rb +74 -0
- data/examples/13_spawn.rb +90 -0
- data/examples/14_rusty_circuit/comic.rb +143 -0
- data/examples/14_rusty_circuit/display.rb +203 -0
- data/examples/14_rusty_circuit/heckler.rb +63 -0
- data/examples/14_rusty_circuit/open_mic.rb +123 -0
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
- data/examples/14_rusty_circuit/scout.rb +156 -0
- data/examples/14_rusty_circuit/scout_notes.md +89 -0
- data/examples/14_rusty_circuit/show.log +234 -0
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
- data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
- data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
- data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
- data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
- data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
- data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
- data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
- data/examples/15_memory_network_and_bus/output/memory.json +13 -0
- data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
- data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
- data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
- data/examples/16_writers_room/display.rb +158 -0
- data/examples/16_writers_room/output/.gitignore +2 -0
- data/examples/16_writers_room/output/opus_001.md +263 -0
- data/examples/16_writers_room/output/opus_001_notes.log +470 -0
- data/examples/16_writers_room/prompts/writer.md +37 -0
- data/examples/16_writers_room/room.rb +150 -0
- data/examples/16_writers_room/tools.rb +162 -0
- data/examples/16_writers_room/writer.rb +121 -0
- data/examples/16_writers_room/writers_room.rb +162 -0
- data/examples/README.md +197 -0
- data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
- data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
- data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
- data/examples/prompts/comedian.md +6 -0
- data/examples/prompts/comedy_critic.md +10 -0
- data/examples/prompts/configurable.md +9 -0
- data/examples/prompts/dispatcher.md +12 -0
- data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
- data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
- data/examples/prompts/frontmatter_mcp_test.md +9 -0
- data/examples/prompts/frontmatter_named_test.md +5 -0
- data/examples/prompts/frontmatter_tools_test.md +6 -0
- data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
- data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
- data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
- data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
- data/examples/prompts/llm_config_demo.md +20 -0
- data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
- data/examples/prompts/os_advocate.md +13 -0
- data/examples/prompts/os_chief.md +13 -0
- data/examples/prompts/os_editor.md +13 -0
- data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
- data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
- data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
- data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
- data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
- data/lib/robot_lab/ask_user.rb +75 -0
- data/lib/robot_lab/config/defaults.yml +121 -0
- data/lib/robot_lab/config.rb +183 -0
- data/lib/robot_lab/error.rb +6 -0
- data/lib/robot_lab/mcp/client.rb +1 -1
- data/lib/robot_lab/memory.rb +10 -34
- data/lib/robot_lab/network.rb +13 -20
- data/lib/robot_lab/robot/bus_messaging.rb +239 -0
- data/lib/robot_lab/robot/mcp_management.rb +88 -0
- data/lib/robot_lab/robot/template_rendering.rb +130 -0
- data/lib/robot_lab/robot.rb +240 -330
- data/lib/robot_lab/robot_message.rb +44 -0
- data/lib/robot_lab/robot_result.rb +1 -0
- data/lib/robot_lab/run_config.rb +184 -0
- data/lib/robot_lab/state_proxy.rb +2 -12
- data/lib/robot_lab/streaming/context.rb +1 -1
- data/lib/robot_lab/task.rb +8 -1
- data/lib/robot_lab/tool.rb +108 -172
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +2 -18
- data/lib/robot_lab/utils.rb +39 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +89 -57
- data/mkdocs.yml +0 -11
- metadata +121 -135
- data/docs/api/adapters/anthropic.md +0 -121
- data/docs/api/adapters/gemini.md +0 -133
- data/docs/api/adapters/index.md +0 -104
- data/docs/api/adapters/openai.md +0 -134
- data/docs/api/history/active-record-adapter.md +0 -195
- data/docs/api/history/config.md +0 -191
- data/docs/api/history/index.md +0 -132
- data/docs/api/history/thread-manager.md +0 -144
- data/docs/guides/history.md +0 -359
- data/examples/prompts/assistant/user.txt.erb +0 -1
- data/examples/prompts/billing/user.txt.erb +0 -1
- data/examples/prompts/classifier/user.txt.erb +0 -1
- data/examples/prompts/entity_extractor/user.txt.erb +0 -3
- data/examples/prompts/escalation/user.txt.erb +0 -34
- data/examples/prompts/general/user.txt.erb +0 -1
- data/examples/prompts/github_assistant/user.txt.erb +0 -1
- data/examples/prompts/helper/user.txt.erb +0 -1
- data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
- data/examples/prompts/order_support/user.txt.erb +0 -22
- data/examples/prompts/product_support/user.txt.erb +0 -32
- data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
- data/examples/prompts/synthesizer/user.txt.erb +0 -15
- data/examples/prompts/technical/user.txt.erb +0 -1
- data/examples/prompts/triage/user.txt.erb +0 -17
- data/lib/robot_lab/adapters/anthropic.rb +0 -163
- data/lib/robot_lab/adapters/base.rb +0 -85
- data/lib/robot_lab/adapters/gemini.rb +0 -193
- data/lib/robot_lab/adapters/openai.rb +0 -159
- data/lib/robot_lab/adapters/registry.rb +0 -81
- data/lib/robot_lab/configuration.rb +0 -143
- data/lib/robot_lab/errors.rb +0 -70
- data/lib/robot_lab/history/active_record_adapter.rb +0 -146
- data/lib/robot_lab/history/config.rb +0 -115
- data/lib/robot_lab/history/thread_manager.rb +0 -93
- data/lib/robot_lab/robotic_model.rb +0 -324
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
Customer <%= customer[:name] %> asks: <%= message %>
|
|
2
|
-
|
|
3
|
-
## Relevant Products
|
|
4
|
-
<% if products&.any? %>
|
|
5
|
-
<% products.each do |product| %>
|
|
6
|
-
### <%= product[:name] %>
|
|
7
|
-
- SKU: <%= product[:sku] %>
|
|
8
|
-
- Price: $<%= product[:price] %>
|
|
9
|
-
- Category: <%= product[:category] %>
|
|
10
|
-
- In Stock: <%= product[:in_stock] ? "Yes (#{product[:quantity]} available)" : "No" %>
|
|
11
|
-
<% if product[:features]&.any? %>
|
|
12
|
-
- Features:
|
|
13
|
-
<% product[:features].each do |feature| %>
|
|
14
|
-
- <%= feature %>
|
|
15
|
-
<% end %>
|
|
16
|
-
<% end %>
|
|
17
|
-
<% if product[:compatible_with]&.any? %>
|
|
18
|
-
- Compatible With: <%= product[:compatible_with].join(", ") %>
|
|
19
|
-
<% end %>
|
|
20
|
-
<% end %>
|
|
21
|
-
<% else %>
|
|
22
|
-
No specific products matched the query. Provide general guidance.
|
|
23
|
-
<% end %>
|
|
24
|
-
|
|
25
|
-
<% if promotions&.any? %>
|
|
26
|
-
## Current Promotions
|
|
27
|
-
<% promotions.each do |promo| %>
|
|
28
|
-
- **<%= promo[:name] %>**: <%= promo[:description] %> (Code: <%= promo[:code] %>)
|
|
29
|
-
<% end %>
|
|
30
|
-
<% end %>
|
|
31
|
-
|
|
32
|
-
Please help the customer with their product question.
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
Please synthesize these analysis results into a comprehensive summary:
|
|
2
|
-
|
|
3
|
-
## Original Text
|
|
4
|
-
<%= message %>
|
|
5
|
-
|
|
6
|
-
## Sentiment Analysis
|
|
7
|
-
<%= sentiment %>
|
|
8
|
-
|
|
9
|
-
## Entities Found
|
|
10
|
-
<%= entities %>
|
|
11
|
-
|
|
12
|
-
## Keywords & Topics
|
|
13
|
-
<%= keywords %>
|
|
14
|
-
|
|
15
|
-
Provide a unified analysis combining all these perspectives.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<%= message %>
|
|
@@ -1,17 +0,0 @@
|
|
|
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.
|
|
@@ -1,163 +0,0 @@
|
|
|
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
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module Adapters
|
|
5
|
-
# Base adapter interface for LLM providers
|
|
6
|
-
#
|
|
7
|
-
# Adapters handle provider-specific message formatting and response parsing.
|
|
8
|
-
# Each provider (Anthropic, OpenAI, Gemini) has different API conventions
|
|
9
|
-
# that the adapter normalizes.
|
|
10
|
-
#
|
|
11
|
-
# @abstract Subclass and implement {#format_messages} and {#parse_response}
|
|
12
|
-
#
|
|
13
|
-
class Base
|
|
14
|
-
# @!attribute [r] provider
|
|
15
|
-
# @return [Symbol] the provider name
|
|
16
|
-
attr_reader :provider
|
|
17
|
-
|
|
18
|
-
# Creates a new adapter instance.
|
|
19
|
-
#
|
|
20
|
-
# @param provider [Symbol] the provider name
|
|
21
|
-
def initialize(provider)
|
|
22
|
-
@provider = provider
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Format internal messages for the provider's API
|
|
26
|
-
#
|
|
27
|
-
# @param messages [Array<Message>] Internal message format
|
|
28
|
-
# @return [Array<Hash>] Provider-specific message format
|
|
29
|
-
#
|
|
30
|
-
def format_messages(messages)
|
|
31
|
-
raise NotImplementedError, "#{self.class}#format_messages must be implemented"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Parse provider response into internal message format
|
|
35
|
-
#
|
|
36
|
-
# @param response [Object] Provider-specific response
|
|
37
|
-
# @return [Array<Message>] Internal message format
|
|
38
|
-
#
|
|
39
|
-
def parse_response(response)
|
|
40
|
-
raise NotImplementedError, "#{self.class}#parse_response must be implemented"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Format tools for the provider's function calling API
|
|
44
|
-
#
|
|
45
|
-
# @param tools [Array<Tool>] Internal tool definitions
|
|
46
|
-
# @return [Array<Hash>] Provider-specific tool format
|
|
47
|
-
#
|
|
48
|
-
def format_tools(tools)
|
|
49
|
-
tools.map(&:to_json_schema)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Format tool choice for the provider
|
|
53
|
-
#
|
|
54
|
-
# @param choice [String, Symbol] "auto", "any", or specific tool name
|
|
55
|
-
# @return [Object] Provider-specific tool choice
|
|
56
|
-
#
|
|
57
|
-
def format_tool_choice(choice)
|
|
58
|
-
case choice.to_s
|
|
59
|
-
when "auto" then "auto"
|
|
60
|
-
when "any" then "required"
|
|
61
|
-
else { type: "function", function: { name: choice.to_s } }
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Extract system message from messages array
|
|
66
|
-
#
|
|
67
|
-
# @param messages [Array<Message>]
|
|
68
|
-
# @return [String, nil]
|
|
69
|
-
#
|
|
70
|
-
def extract_system_message(messages)
|
|
71
|
-
system_msg = messages.find(&:system?)
|
|
72
|
-
system_msg&.content
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Filter out system messages
|
|
76
|
-
#
|
|
77
|
-
# @param messages [Array<Message>]
|
|
78
|
-
# @return [Array<Message>]
|
|
79
|
-
#
|
|
80
|
-
def conversation_messages(messages)
|
|
81
|
-
messages.reject(&:system?)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module Adapters
|
|
5
|
-
# Adapter for Google Gemini models
|
|
6
|
-
#
|
|
7
|
-
# Handles Gemini-specific API conventions:
|
|
8
|
-
# - Role mapping (assistant -> model)
|
|
9
|
-
# - Contents/parts array structure
|
|
10
|
-
# - functionCall/functionResponse format
|
|
11
|
-
#
|
|
12
|
-
class Gemini < Base
|
|
13
|
-
# Creates a new Gemini adapter instance.
|
|
14
|
-
def initialize
|
|
15
|
-
super(:gemini)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Format messages for Gemini API
|
|
19
|
-
#
|
|
20
|
-
# Gemini uses "model" role instead of "assistant" and structures
|
|
21
|
-
# content as parts arrays.
|
|
22
|
-
#
|
|
23
|
-
# @param messages [Array<Message>]
|
|
24
|
-
# @return [Array<Hash>]
|
|
25
|
-
#
|
|
26
|
-
def format_messages(messages)
|
|
27
|
-
# Gemini handles system messages differently - as system_instruction
|
|
28
|
-
conversation_messages(messages).map { |msg| format_single_message(msg) }
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Parse Gemini response into internal messages
|
|
32
|
-
#
|
|
33
|
-
# @param response [RubyLLM::Response]
|
|
34
|
-
# @return [Array<Message>]
|
|
35
|
-
#
|
|
36
|
-
def parse_response(response)
|
|
37
|
-
messages = []
|
|
38
|
-
|
|
39
|
-
# Handle text content
|
|
40
|
-
if response.content && !response.content.empty?
|
|
41
|
-
messages << TextMessage.new(
|
|
42
|
-
role: "assistant",
|
|
43
|
-
content: response.content,
|
|
44
|
-
stop_reason: response.tool_calls&.any? ? "tool" : "stop"
|
|
45
|
-
)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Handle function calls
|
|
49
|
-
if response.tool_calls&.any?
|
|
50
|
-
tool_messages = response.tool_calls.map do |id, tool_call|
|
|
51
|
-
ToolMessage.new(
|
|
52
|
-
id: id,
|
|
53
|
-
name: tool_call.name,
|
|
54
|
-
input: parse_tool_arguments(tool_call.arguments)
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
messages << ToolCallMessage.new(
|
|
59
|
-
role: "assistant",
|
|
60
|
-
tools: tool_messages,
|
|
61
|
-
stop_reason: "tool"
|
|
62
|
-
)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
messages
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Format tools for Gemini function declarations
|
|
69
|
-
#
|
|
70
|
-
# Gemini doesn't support additionalProperties in schemas
|
|
71
|
-
#
|
|
72
|
-
# @param tools [Array<Tool>]
|
|
73
|
-
# @return [Array<Hash>]
|
|
74
|
-
#
|
|
75
|
-
def format_tools(tools)
|
|
76
|
-
tools.map do |tool|
|
|
77
|
-
schema = tool.to_json_schema
|
|
78
|
-
params = clean_schema_for_gemini(schema[:parameters] || { type: "object", properties: {} })
|
|
79
|
-
{
|
|
80
|
-
name: schema[:name],
|
|
81
|
-
description: schema[:description],
|
|
82
|
-
parameters: params
|
|
83
|
-
}
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Gemini tool choice format
|
|
88
|
-
#
|
|
89
|
-
# @param choice [String, Symbol]
|
|
90
|
-
# @return [Hash]
|
|
91
|
-
#
|
|
92
|
-
def format_tool_choice(choice)
|
|
93
|
-
case choice.to_s
|
|
94
|
-
when "auto" then { mode: "AUTO" }
|
|
95
|
-
when "any" then { mode: "ANY" }
|
|
96
|
-
when "none" then { mode: "NONE" }
|
|
97
|
-
else { mode: "ANY", allowed_function_names: [choice.to_s] }
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
private
|
|
102
|
-
|
|
103
|
-
def format_single_message(msg)
|
|
104
|
-
role = gemini_role(msg.role)
|
|
105
|
-
|
|
106
|
-
case msg
|
|
107
|
-
when TextMessage
|
|
108
|
-
{
|
|
109
|
-
role: role,
|
|
110
|
-
parts: [{ text: msg.content }]
|
|
111
|
-
}
|
|
112
|
-
when ToolCallMessage
|
|
113
|
-
{
|
|
114
|
-
role: "model",
|
|
115
|
-
parts: msg.tools.map do |tool|
|
|
116
|
-
{
|
|
117
|
-
functionCall: {
|
|
118
|
-
name: tool.name,
|
|
119
|
-
args: tool.input
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
end
|
|
123
|
-
}
|
|
124
|
-
when ToolResultMessage
|
|
125
|
-
{
|
|
126
|
-
role: "user",
|
|
127
|
-
parts: [
|
|
128
|
-
{
|
|
129
|
-
functionResponse: {
|
|
130
|
-
name: msg.tool.name,
|
|
131
|
-
response: format_tool_result_content(msg.content)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
]
|
|
135
|
-
}
|
|
136
|
-
else
|
|
137
|
-
{ role: role, parts: [{ text: msg.content.to_s }] }
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def gemini_role(role)
|
|
142
|
-
case role.to_s
|
|
143
|
-
when "assistant" then "model"
|
|
144
|
-
when "system" then "user" # Gemini handles system as system_instruction
|
|
145
|
-
else role.to_s
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def format_tool_result_content(content)
|
|
150
|
-
case content
|
|
151
|
-
when Hash
|
|
152
|
-
content
|
|
153
|
-
when String
|
|
154
|
-
{ result: content }
|
|
155
|
-
else
|
|
156
|
-
{ result: content.to_s }
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def parse_tool_arguments(arguments)
|
|
161
|
-
case arguments
|
|
162
|
-
when String
|
|
163
|
-
begin
|
|
164
|
-
JSON.parse(arguments, symbolize_names: true)
|
|
165
|
-
rescue JSON::ParserError
|
|
166
|
-
{ raw: arguments }
|
|
167
|
-
end
|
|
168
|
-
when Hash
|
|
169
|
-
arguments.transform_keys(&:to_sym)
|
|
170
|
-
else
|
|
171
|
-
arguments || {}
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Remove additionalProperties which Gemini doesn't support
|
|
176
|
-
def clean_schema_for_gemini(schema)
|
|
177
|
-
return schema unless schema.is_a?(Hash)
|
|
178
|
-
|
|
179
|
-
cleaned = schema.dup
|
|
180
|
-
cleaned.delete(:additionalProperties)
|
|
181
|
-
cleaned.delete("additionalProperties")
|
|
182
|
-
|
|
183
|
-
if cleaned[:properties]
|
|
184
|
-
cleaned[:properties] = cleaned[:properties].transform_values do |prop|
|
|
185
|
-
clean_schema_for_gemini(prop)
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
cleaned
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
end
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RobotLab
|
|
4
|
-
module Adapters
|
|
5
|
-
# Adapter for OpenAI GPT models
|
|
6
|
-
#
|
|
7
|
-
# Handles OpenAI-specific API conventions:
|
|
8
|
-
# - Function calling format
|
|
9
|
-
# - Strict mode for structured outputs
|
|
10
|
-
# - finish_reason to stop_reason mapping
|
|
11
|
-
#
|
|
12
|
-
class OpenAI < Base
|
|
13
|
-
# Creates a new OpenAI adapter instance.
|
|
14
|
-
def initialize
|
|
15
|
-
super(:openai)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Format messages for OpenAI API
|
|
19
|
-
#
|
|
20
|
-
# @param messages [Array<Message>]
|
|
21
|
-
# @return [Array<Hash>]
|
|
22
|
-
#
|
|
23
|
-
def format_messages(messages)
|
|
24
|
-
messages.map { |msg| format_single_message(msg) }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Parse OpenAI response into internal messages
|
|
28
|
-
#
|
|
29
|
-
# @param response [RubyLLM::Response]
|
|
30
|
-
# @return [Array<Message>]
|
|
31
|
-
#
|
|
32
|
-
def parse_response(response)
|
|
33
|
-
messages = []
|
|
34
|
-
|
|
35
|
-
# Handle text content
|
|
36
|
-
if response.content && !response.content.empty?
|
|
37
|
-
messages << TextMessage.new(
|
|
38
|
-
role: "assistant",
|
|
39
|
-
content: response.content,
|
|
40
|
-
stop_reason: response.tool_calls&.any? ? "tool" : "stop"
|
|
41
|
-
)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Handle tool calls
|
|
45
|
-
if response.tool_calls&.any?
|
|
46
|
-
tool_messages = response.tool_calls.map do |id, tool_call|
|
|
47
|
-
ToolMessage.new(
|
|
48
|
-
id: id,
|
|
49
|
-
name: tool_call.name,
|
|
50
|
-
input: parse_tool_arguments(tool_call.arguments)
|
|
51
|
-
)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
messages << ToolCallMessage.new(
|
|
55
|
-
role: "assistant",
|
|
56
|
-
tools: tool_messages,
|
|
57
|
-
stop_reason: "tool"
|
|
58
|
-
)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
messages
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Format tools for OpenAI function calling
|
|
65
|
-
#
|
|
66
|
-
# @param tools [Array<Tool>]
|
|
67
|
-
# @return [Array<Hash>]
|
|
68
|
-
#
|
|
69
|
-
def format_tools(tools)
|
|
70
|
-
tools.map do |tool|
|
|
71
|
-
schema = tool.to_json_schema
|
|
72
|
-
{
|
|
73
|
-
type: "function",
|
|
74
|
-
function: {
|
|
75
|
-
name: schema[:name],
|
|
76
|
-
description: schema[:description],
|
|
77
|
-
parameters: schema[:parameters] || { type: "object", properties: {} },
|
|
78
|
-
strict: tool.strict.nil? ? true : tool.strict
|
|
79
|
-
}.compact
|
|
80
|
-
}
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# OpenAI tool choice format
|
|
85
|
-
#
|
|
86
|
-
# @param choice [String, Symbol]
|
|
87
|
-
# @return [String, Hash]
|
|
88
|
-
#
|
|
89
|
-
def format_tool_choice(choice)
|
|
90
|
-
case choice.to_s
|
|
91
|
-
when "auto" then "auto"
|
|
92
|
-
when "any" then "required"
|
|
93
|
-
when "none" then "none"
|
|
94
|
-
else { type: "function", function: { 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: nil,
|
|
108
|
-
tool_calls: msg.tools.map do |tool|
|
|
109
|
-
{
|
|
110
|
-
id: tool.id,
|
|
111
|
-
type: "function",
|
|
112
|
-
function: {
|
|
113
|
-
name: tool.name,
|
|
114
|
-
arguments: JSON.generate(tool.input)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
end
|
|
118
|
-
}
|
|
119
|
-
when ToolResultMessage
|
|
120
|
-
{
|
|
121
|
-
role: "tool",
|
|
122
|
-
tool_call_id: msg.tool.id,
|
|
123
|
-
content: format_tool_result_content(msg.content)
|
|
124
|
-
}
|
|
125
|
-
else
|
|
126
|
-
{ role: msg.role, content: msg.content.to_s }
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def format_tool_result_content(content)
|
|
131
|
-
case content
|
|
132
|
-
when Hash
|
|
133
|
-
JSON.generate(content)
|
|
134
|
-
when String
|
|
135
|
-
content
|
|
136
|
-
else
|
|
137
|
-
content.to_s
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def parse_tool_arguments(arguments)
|
|
142
|
-
case arguments
|
|
143
|
-
when String
|
|
144
|
-
# Handle OpenAI's backtick wrapping quirk
|
|
145
|
-
cleaned = arguments.gsub(/\A```(?:json)?\n?/, "").gsub(/\n?```\z/, "")
|
|
146
|
-
begin
|
|
147
|
-
JSON.parse(cleaned, symbolize_names: true)
|
|
148
|
-
rescue JSON::ParserError
|
|
149
|
-
{ raw: arguments }
|
|
150
|
-
end
|
|
151
|
-
when Hash
|
|
152
|
-
arguments.transform_keys(&:to_sym)
|
|
153
|
-
else
|
|
154
|
-
arguments || {}
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
end
|