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.
Files changed (187) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +9 -9
  3. data/.irbrc +6 -0
  4. data/CHANGELOG.md +140 -0
  5. data/README.md +263 -48
  6. data/Rakefile +71 -1
  7. data/docs/api/core/index.md +53 -46
  8. data/docs/api/core/memory.md +200 -154
  9. data/docs/api/core/network.md +13 -3
  10. data/docs/api/core/robot.md +490 -130
  11. data/docs/api/core/state.md +55 -73
  12. data/docs/api/core/tool.md +205 -209
  13. data/docs/api/index.md +7 -28
  14. data/docs/api/mcp/client.md +119 -48
  15. data/docs/api/mcp/index.md +75 -60
  16. data/docs/api/mcp/server.md +120 -136
  17. data/docs/api/mcp/transports.md +172 -184
  18. data/docs/api/messages/index.md +35 -20
  19. data/docs/api/messages/text-message.md +67 -21
  20. data/docs/api/messages/tool-call-message.md +80 -41
  21. data/docs/api/messages/tool-result-message.md +119 -50
  22. data/docs/api/messages/user-message.md +48 -24
  23. data/docs/api/streaming/context.md +157 -74
  24. data/docs/api/streaming/events.md +114 -166
  25. data/docs/api/streaming/index.md +74 -72
  26. data/docs/architecture/core-concepts.md +360 -116
  27. data/docs/architecture/index.md +97 -59
  28. data/docs/architecture/message-flow.md +138 -129
  29. data/docs/architecture/network-orchestration.md +197 -50
  30. data/docs/architecture/robot-execution.md +199 -146
  31. data/docs/architecture/state-management.md +255 -187
  32. data/docs/concepts.md +311 -49
  33. data/docs/examples/basic-chat.md +89 -77
  34. data/docs/examples/index.md +222 -47
  35. data/docs/examples/mcp-server.md +207 -203
  36. data/docs/examples/multi-robot-network.md +129 -35
  37. data/docs/examples/rails-application.md +159 -160
  38. data/docs/examples/tool-usage.md +295 -204
  39. data/docs/getting-started/configuration.md +347 -154
  40. data/docs/getting-started/index.md +1 -1
  41. data/docs/getting-started/installation.md +22 -13
  42. data/docs/getting-started/quick-start.md +166 -121
  43. data/docs/guides/building-robots.md +418 -212
  44. data/docs/guides/creating-networks.md +143 -24
  45. data/docs/guides/index.md +0 -5
  46. data/docs/guides/mcp-integration.md +152 -113
  47. data/docs/guides/memory.md +220 -164
  48. data/docs/guides/rails-integration.md +244 -162
  49. data/docs/guides/streaming.md +137 -187
  50. data/docs/guides/using-tools.md +259 -212
  51. data/docs/index.md +46 -41
  52. data/examples/01_simple_robot.rb +6 -9
  53. data/examples/02_tools.rb +6 -9
  54. data/examples/03_network.rb +19 -17
  55. data/examples/04_mcp.rb +5 -8
  56. data/examples/05_streaming.rb +5 -8
  57. data/examples/06_prompt_templates.rb +42 -37
  58. data/examples/07_network_memory.rb +13 -14
  59. data/examples/08_llm_config.rb +169 -0
  60. data/examples/09_chaining.rb +262 -0
  61. data/examples/10_memory.rb +331 -0
  62. data/examples/11_network_introspection.rb +253 -0
  63. data/examples/12_message_bus.rb +74 -0
  64. data/examples/13_spawn.rb +90 -0
  65. data/examples/14_rusty_circuit/comic.rb +143 -0
  66. data/examples/14_rusty_circuit/display.rb +203 -0
  67. data/examples/14_rusty_circuit/heckler.rb +63 -0
  68. data/examples/14_rusty_circuit/open_mic.rb +123 -0
  69. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
  70. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
  71. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
  72. data/examples/14_rusty_circuit/scout.rb +156 -0
  73. data/examples/14_rusty_circuit/scout_notes.md +89 -0
  74. data/examples/14_rusty_circuit/show.log +234 -0
  75. data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
  76. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
  77. data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
  78. data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
  79. data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
  80. data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
  81. data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
  82. data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
  83. data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
  84. data/examples/15_memory_network_and_bus/output/memory.json +13 -0
  85. data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
  86. data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
  87. data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
  88. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
  89. data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
  90. data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
  91. data/examples/16_writers_room/display.rb +158 -0
  92. data/examples/16_writers_room/output/.gitignore +2 -0
  93. data/examples/16_writers_room/output/opus_001.md +263 -0
  94. data/examples/16_writers_room/output/opus_001_notes.log +470 -0
  95. data/examples/16_writers_room/prompts/writer.md +37 -0
  96. data/examples/16_writers_room/room.rb +150 -0
  97. data/examples/16_writers_room/tools.rb +162 -0
  98. data/examples/16_writers_room/writer.rb +121 -0
  99. data/examples/16_writers_room/writers_room.rb +162 -0
  100. data/examples/README.md +197 -0
  101. data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
  102. data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
  103. data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
  104. data/examples/prompts/comedian.md +6 -0
  105. data/examples/prompts/comedy_critic.md +10 -0
  106. data/examples/prompts/configurable.md +9 -0
  107. data/examples/prompts/dispatcher.md +12 -0
  108. data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
  109. data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
  110. data/examples/prompts/frontmatter_mcp_test.md +9 -0
  111. data/examples/prompts/frontmatter_named_test.md +5 -0
  112. data/examples/prompts/frontmatter_tools_test.md +6 -0
  113. data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
  114. data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
  115. data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
  116. data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
  117. data/examples/prompts/llm_config_demo.md +20 -0
  118. data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
  119. data/examples/prompts/os_advocate.md +13 -0
  120. data/examples/prompts/os_chief.md +13 -0
  121. data/examples/prompts/os_editor.md +13 -0
  122. data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
  123. data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
  124. data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
  125. data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
  126. data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
  127. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
  128. data/lib/robot_lab/ask_user.rb +75 -0
  129. data/lib/robot_lab/config/defaults.yml +121 -0
  130. data/lib/robot_lab/config.rb +183 -0
  131. data/lib/robot_lab/error.rb +6 -0
  132. data/lib/robot_lab/mcp/client.rb +1 -1
  133. data/lib/robot_lab/memory.rb +10 -34
  134. data/lib/robot_lab/network.rb +13 -20
  135. data/lib/robot_lab/robot/bus_messaging.rb +239 -0
  136. data/lib/robot_lab/robot/mcp_management.rb +88 -0
  137. data/lib/robot_lab/robot/template_rendering.rb +130 -0
  138. data/lib/robot_lab/robot.rb +240 -330
  139. data/lib/robot_lab/robot_message.rb +44 -0
  140. data/lib/robot_lab/robot_result.rb +1 -0
  141. data/lib/robot_lab/run_config.rb +184 -0
  142. data/lib/robot_lab/state_proxy.rb +2 -12
  143. data/lib/robot_lab/streaming/context.rb +1 -1
  144. data/lib/robot_lab/task.rb +8 -1
  145. data/lib/robot_lab/tool.rb +108 -172
  146. data/lib/robot_lab/tool_config.rb +1 -1
  147. data/lib/robot_lab/tool_manifest.rb +2 -18
  148. data/lib/robot_lab/utils.rb +39 -0
  149. data/lib/robot_lab/version.rb +1 -1
  150. data/lib/robot_lab.rb +89 -57
  151. data/mkdocs.yml +0 -11
  152. metadata +121 -135
  153. data/docs/api/adapters/anthropic.md +0 -121
  154. data/docs/api/adapters/gemini.md +0 -133
  155. data/docs/api/adapters/index.md +0 -104
  156. data/docs/api/adapters/openai.md +0 -134
  157. data/docs/api/history/active-record-adapter.md +0 -195
  158. data/docs/api/history/config.md +0 -191
  159. data/docs/api/history/index.md +0 -132
  160. data/docs/api/history/thread-manager.md +0 -144
  161. data/docs/guides/history.md +0 -359
  162. data/examples/prompts/assistant/user.txt.erb +0 -1
  163. data/examples/prompts/billing/user.txt.erb +0 -1
  164. data/examples/prompts/classifier/user.txt.erb +0 -1
  165. data/examples/prompts/entity_extractor/user.txt.erb +0 -3
  166. data/examples/prompts/escalation/user.txt.erb +0 -34
  167. data/examples/prompts/general/user.txt.erb +0 -1
  168. data/examples/prompts/github_assistant/user.txt.erb +0 -1
  169. data/examples/prompts/helper/user.txt.erb +0 -1
  170. data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
  171. data/examples/prompts/order_support/user.txt.erb +0 -22
  172. data/examples/prompts/product_support/user.txt.erb +0 -32
  173. data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
  174. data/examples/prompts/synthesizer/user.txt.erb +0 -15
  175. data/examples/prompts/technical/user.txt.erb +0 -1
  176. data/examples/prompts/triage/user.txt.erb +0 -17
  177. data/lib/robot_lab/adapters/anthropic.rb +0 -163
  178. data/lib/robot_lab/adapters/base.rb +0 -85
  179. data/lib/robot_lab/adapters/gemini.rb +0 -193
  180. data/lib/robot_lab/adapters/openai.rb +0 -159
  181. data/lib/robot_lab/adapters/registry.rb +0 -81
  182. data/lib/robot_lab/configuration.rb +0 -143
  183. data/lib/robot_lab/errors.rb +0 -70
  184. data/lib/robot_lab/history/active_record_adapter.rb +0 -146
  185. data/lib/robot_lab/history/config.rb +0 -115
  186. data/lib/robot_lab/history/thread_manager.rb +0 -93
  187. 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,3 +0,0 @@
1
- Analyze the sentiment of this text:
2
-
3
- <%= message %>
@@ -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