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,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RobotLab
4
- module Adapters
5
- # Registry for looking up provider adapters
6
- #
7
- # Maps provider symbols to their adapter classes.
8
- #
9
- # @example
10
- # adapter = Registry.for(:anthropic)
11
- # adapter.format_messages(messages)
12
- #
13
- module Registry
14
- # @return [Hash<Symbol, Class>] mapping of provider symbols to adapter classes
15
- ADAPTERS = {
16
- anthropic: Anthropic,
17
- openai: OpenAI,
18
- gemini: Gemini,
19
- # Azure uses OpenAI adapter
20
- azure_openai: OpenAI,
21
- # Grok uses OpenAI adapter
22
- grok: OpenAI,
23
- # Ollama uses OpenAI adapter
24
- ollama: OpenAI,
25
- # OpenRouter uses OpenAI adapter
26
- openrouter: OpenAI,
27
- # Bedrock uses Anthropic adapter
28
- bedrock: Anthropic,
29
- # VertexAI uses Gemini adapter
30
- vertexai: Gemini
31
- }.freeze
32
-
33
- class << self
34
- # Get adapter for a provider
35
- #
36
- # @param provider [Symbol, String] Provider name
37
- # @return [Base] Adapter instance
38
- # @raise [ArgumentError] If provider not found
39
- #
40
- def for(provider)
41
- provider_sym = provider.to_s.downcase.gsub("-", "_").to_sym
42
- adapter_class = ADAPTERS[provider_sym]
43
-
44
- unless adapter_class
45
- raise ArgumentError, "Unknown provider: #{provider}. " \
46
- "Available providers: #{available.join(', ')}"
47
- end
48
-
49
- adapter_class.new
50
- end
51
-
52
- # List available providers
53
- #
54
- # @return [Array<Symbol>]
55
- #
56
- def available
57
- ADAPTERS.keys
58
- end
59
-
60
- # Check if provider is supported
61
- #
62
- # @param provider [Symbol, String]
63
- # @return [Boolean]
64
- #
65
- def supports?(provider)
66
- provider_sym = provider.to_s.downcase.gsub("-", "_").to_sym
67
- ADAPTERS.key?(provider_sym)
68
- end
69
-
70
- # Register a custom adapter
71
- #
72
- # @param provider [Symbol] Provider name
73
- # @param adapter_class [Class] Adapter class
74
- #
75
- def register(provider, adapter_class)
76
- ADAPTERS[provider.to_sym] = adapter_class
77
- end
78
- end
79
- end
80
- end
81
- end
@@ -1,143 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RobotLab
4
- # Global configuration for RobotLab
5
- #
6
- # @example
7
- # RobotLab.configure do |config|
8
- # config.default_provider = :anthropic
9
- # config.default_model = "claude-sonnet-4"
10
- # config.template_path = "app/prompts"
11
- # config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
12
- #
13
- # # Global MCP servers available to all networks and robots
14
- # config.mcp = [
15
- # { name: "github", transport: { type: "stdio", command: "github-mcp" } }
16
- # ]
17
- #
18
- # # Global tools whitelist (only these tools are available)
19
- # config.tools = %w[search_code create_issue]
20
- # end
21
- #
22
- class Configuration
23
- # @!attribute [rw] default_provider
24
- # @return [Symbol] the default LLM provider (defaults to :anthropic)
25
- # @!attribute [rw] default_model
26
- # @return [String] the default model to use (defaults to "claude-sonnet-4")
27
- # @!attribute [rw] max_iterations
28
- # @return [Integer] maximum robot iterations per network run (defaults to 10)
29
- # @!attribute [rw] max_tool_iterations
30
- # @return [Integer] maximum tool iterations per robot run (defaults to 10)
31
- # @!attribute [rw] streaming_enabled
32
- # @return [Boolean] whether streaming is enabled by default (defaults to true)
33
- # @!attribute [rw] logger
34
- # @return [Logger] the logger instance
35
- # @!attribute [rw] mcp
36
- # @return [Symbol, Array] global MCP server configuration (:none, :inherit, or array)
37
- # @!attribute [rw] tools
38
- # @return [Symbol, Array] global tools whitelist (:none, :inherit, or array)
39
- attr_accessor :default_provider,
40
- :default_model,
41
- :max_iterations,
42
- :max_tool_iterations,
43
- :streaming_enabled,
44
- :logger,
45
- :mcp,
46
- :tools
47
-
48
- # Creates a new Configuration with default values.
49
- def initialize
50
- @default_provider = :anthropic
51
- @default_model = "claude-sonnet-4"
52
- @max_iterations = 10
53
- @max_tool_iterations = 10
54
- @streaming_enabled = true
55
- @logger = default_logger
56
- @template_path = nil
57
- @mcp = :none
58
- @tools = :none
59
- end
60
-
61
- # Sets the Anthropic API key.
62
- #
63
- # @param key [String] the API key
64
- # @return [void]
65
- def anthropic_api_key=(key)
66
- RubyLLM.configure { |c| c.anthropic_api_key = key }
67
- end
68
-
69
- # Sets the OpenAI API key.
70
- #
71
- # @param key [String] the API key
72
- # @return [void]
73
- def openai_api_key=(key)
74
- RubyLLM.configure { |c| c.openai_api_key = key }
75
- end
76
-
77
- # Sets the Google Gemini API key.
78
- #
79
- # @param key [String] the API key
80
- # @return [void]
81
- def gemini_api_key=(key)
82
- RubyLLM.configure { |c| c.gemini_api_key = key }
83
- end
84
-
85
- # Sets the AWS Bedrock API key.
86
- #
87
- # @param key [String] the API key
88
- # @return [void]
89
- def bedrock_api_key=(key)
90
- RubyLLM.configure { |c| c.bedrock_api_key = key }
91
- end
92
-
93
- # Sets the OpenRouter API key.
94
- #
95
- # @param key [String] the API key
96
- # @return [void]
97
- def openrouter_api_key=(key)
98
- RubyLLM.configure { |c| c.openrouter_api_key = key }
99
- end
100
-
101
- # Set the template path and configure ruby_llm-template
102
- #
103
- # @param path [String] Path to the templates directory
104
- #
105
- def template_path=(path)
106
- @template_path = path
107
- configure_template_library if path
108
- end
109
-
110
- # Returns the template path.
111
- #
112
- # @return [String] the configured template path or default
113
- def template_path
114
- @template_path || default_template_path
115
- end
116
-
117
- private
118
-
119
- def configure_template_library
120
- require "ruby_llm/template"
121
- RubyLLM::Template.configure do |config|
122
- config.template_directory = @template_path
123
- end
124
- end
125
-
126
- def default_template_path
127
- if defined?(Rails) && Rails.root
128
- Rails.root.join("app", "prompts").to_s
129
- else
130
- "prompts"
131
- end
132
- end
133
-
134
- def default_logger
135
- if defined?(Rails) && Rails.respond_to?(:logger)
136
- Rails.logger
137
- else
138
- require "logger"
139
- Logger.new($stdout, level: Logger::INFO)
140
- end
141
- end
142
- end
143
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RobotLab
4
- # Error serialization utilities
5
- #
6
- # Provides methods to serialize Ruby exceptions into a format
7
- # suitable for tool results and logging.
8
- #
9
- module Errors
10
- class << self
11
- # Serialize an exception to a hash
12
- #
13
- # @param error [Exception] The error to serialize
14
- # @param include_backtrace [Boolean] Whether to include backtrace
15
- # @return [Hash] Serialized error
16
- #
17
- def serialize(error, include_backtrace: false)
18
- result = {
19
- type: error.class.name,
20
- message: error.message
21
- }
22
-
23
- if include_backtrace && error.backtrace
24
- result[:backtrace] = error.backtrace.first(10)
25
- end
26
-
27
- if error.cause
28
- result[:cause] = serialize(error.cause, include_backtrace: include_backtrace)
29
- end
30
-
31
- result
32
- end
33
-
34
- # Deserialize an error hash back to an exception
35
- #
36
- # @param hash [Hash] Serialized error
37
- # @return [StandardError]
38
- #
39
- def deserialize(hash)
40
- hash = hash.transform_keys(&:to_sym)
41
- klass = begin
42
- Object.const_get(hash[:type])
43
- rescue NameError
44
- StandardError
45
- end
46
- klass.new(hash[:message])
47
- end
48
-
49
- # Format error for display
50
- #
51
- # @param error [Exception] The error
52
- # @return [String]
53
- #
54
- def format(error)
55
- "[#{error.class.name}] #{error.message}"
56
- end
57
-
58
- # Wrap a block and return error hash on failure
59
- #
60
- # @yield Block to execute
61
- # @return [Hash] { data: result } or { error: serialized_error }
62
- #
63
- def capture
64
- { data: yield }
65
- rescue StandardError => e
66
- { error: serialize(e) }
67
- end
68
- end
69
- end
70
- end
@@ -1,146 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RobotLab
4
- module History
5
- # ActiveRecord-based history persistence adapter
6
- #
7
- # Provides thread and result storage using ActiveRecord models.
8
- # Requires Rails or standalone ActiveRecord setup.
9
- #
10
- # @example
11
- # adapter = ActiveRecordAdapter.new(
12
- # thread_model: RobotLabThread,
13
- # result_model: RobotLabResult
14
- # )
15
- #
16
- # config = adapter.to_config
17
- # network = RobotLab.create_network(history: config)
18
- #
19
- class ActiveRecordAdapter
20
- # @!attribute [r] thread_model
21
- # @return [Class] ActiveRecord model class for threads
22
- # @!attribute [r] result_model
23
- # @return [Class] ActiveRecord model class for results
24
- attr_reader :thread_model, :result_model
25
-
26
- # Initialize adapter with ActiveRecord models
27
- #
28
- # @param thread_model [Class] ActiveRecord model for threads
29
- # @param result_model [Class] ActiveRecord model for results
30
- #
31
- def initialize(thread_model:, result_model:)
32
- @thread_model = thread_model
33
- @result_model = result_model
34
- end
35
-
36
- # Create a new thread
37
- #
38
- # @param state [State] Current state
39
- # @param input [String, UserMessage] Initial input
40
- # @return [Hash] Thread ID and metadata
41
- #
42
- def create_thread(state:, input:, **)
43
- input_content = input.is_a?(UserMessage) ? input.content : input.to_s
44
- input_metadata = input.is_a?(UserMessage) ? input.metadata : {}
45
-
46
- thread = @thread_model.create!(
47
- session_id: SecureRandom.uuid,
48
- initial_input: input_content,
49
- input_metadata: input_metadata,
50
- state_data: state.data.to_h
51
- )
52
-
53
- { session_id: thread.session_id, created_at: thread.created_at }
54
- end
55
-
56
- # Retrieve results for a thread
57
- #
58
- # @param session_id [String] Thread identifier
59
- # @return [Array<RobotResult>] History of results
60
- #
61
- def get(session_id:, **)
62
- @result_model
63
- .where(session_id: session_id)
64
- .order(:sequence_number, :created_at)
65
- .map { |record| deserialize_result(record) }
66
- end
67
-
68
- # Append user message to thread
69
- #
70
- # @param session_id [String] Thread identifier
71
- # @param message [UserMessage] Message to append
72
- #
73
- def append_user_message(session_id:, message:, **)
74
- @thread_model.where(session_id: session_id).update_all(
75
- last_user_message: message.content,
76
- last_user_message_at: Time.current
77
- )
78
- end
79
-
80
- # Append results to thread
81
- #
82
- # @param session_id [String] Thread identifier
83
- # @param new_results [Array<RobotResult>] Results to append
84
- #
85
- def append_results(session_id:, new_results:, **)
86
- base_sequence = @result_model.where(session_id: session_id).maximum(:sequence_number) || 0
87
-
88
- new_results.each_with_index do |result, index|
89
- @result_model.create!(
90
- session_id: session_id,
91
- robot_name: result.robot_name,
92
- sequence_number: base_sequence + index + 1,
93
- output_data: serialize_messages(result.output),
94
- tool_calls_data: serialize_messages(result.tool_calls),
95
- stop_reason: result.stop_reason,
96
- checksum: result.checksum
97
- )
98
- end
99
-
100
- # Update thread timestamp
101
- @thread_model.where(session_id: session_id).update_all(
102
- updated_at: Time.current
103
- )
104
- end
105
-
106
- # Convert adapter to Config object
107
- #
108
- # @return [Config] History configuration
109
- #
110
- def to_config
111
- Config.new(
112
- create_thread: method(:create_thread),
113
- get: method(:get),
114
- append_user_message: method(:append_user_message),
115
- append_results: method(:append_results)
116
- )
117
- end
118
-
119
- private
120
-
121
- def serialize_messages(messages)
122
- messages.map(&:to_h)
123
- end
124
-
125
- def deserialize_result(record)
126
- output = deserialize_messages(record.output_data)
127
- tool_calls = deserialize_messages(record.tool_calls_data)
128
-
129
- RobotResult.new(
130
- robot_name: record.robot_name,
131
- output: output,
132
- tool_calls: tool_calls,
133
- stop_reason: record.stop_reason
134
- )
135
- end
136
-
137
- def deserialize_messages(data)
138
- return [] unless data
139
-
140
- data.map do |msg_hash|
141
- Message.from_hash(msg_hash.symbolize_keys)
142
- end
143
- end
144
- end
145
- end
146
- end
@@ -1,115 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RobotLab
4
- module History
5
- # Configuration for conversation history persistence
6
- #
7
- # Defines callbacks for creating threads, retrieving history,
8
- # and appending messages/results.
9
- #
10
- # @example
11
- # config = History::Config.new(
12
- # create_thread: ->(state:, input:, **) {
13
- # { session_id: SecureRandom.uuid }
14
- # },
15
- # get: ->(session_id:, **) {
16
- # database.find_results(session_id)
17
- # },
18
- # append_results: ->(session_id:, new_results:, **) {
19
- # database.insert_results(session_id, new_results)
20
- # }
21
- # )
22
- #
23
- class Config
24
- # @!attribute [rw] create_thread
25
- # @return [Proc, nil] callback to create a new conversation thread
26
- # @!attribute [rw] get
27
- # @return [Proc, nil] callback to retrieve history for a thread
28
- # @!attribute [rw] append_user_message
29
- # @return [Proc, nil] callback to append user messages
30
- # @!attribute [rw] append_results
31
- # @return [Proc, nil] callback to append robot results
32
- attr_accessor :create_thread, :get, :append_user_message, :append_results
33
-
34
- # Initialize history configuration
35
- #
36
- # @param create_thread [Proc] Callback to create a new thread
37
- # @param get [Proc] Callback to retrieve history for a thread
38
- # @param append_user_message [Proc] Callback to append user messages
39
- # @param append_results [Proc] Callback to append robot results
40
- #
41
- def initialize(create_thread: nil, get: nil, append_user_message: nil, append_results: nil)
42
- @create_thread = create_thread
43
- @get = get
44
- @append_user_message = append_user_message
45
- @append_results = append_results
46
- end
47
-
48
- # Check if history persistence is configured
49
- #
50
- # @return [Boolean]
51
- #
52
- def configured?
53
- @create_thread && @get
54
- end
55
-
56
- # Create a new conversation thread
57
- #
58
- # @param state [State] Current state
59
- # @param input [String, UserMessage] Initial input
60
- # @param kwargs [Hash] Additional arguments
61
- # @return [Hash] Must include :session_id
62
- #
63
- def create_thread!(state:, input:, **kwargs)
64
- raise HistoryError, "create_thread callback not configured" unless @create_thread
65
-
66
- result = @create_thread.call(state: state, input: input, **kwargs)
67
-
68
- unless result.is_a?(Hash) && result[:session_id]
69
- raise HistoryError, "create_thread must return a hash with :session_id"
70
- end
71
-
72
- result
73
- end
74
-
75
- # Retrieve history for a thread
76
- #
77
- # @param session_id [String] Thread identifier
78
- # @param kwargs [Hash] Additional arguments
79
- # @return [Array<RobotResult>] History of results
80
- #
81
- def get!(session_id:, **kwargs)
82
- raise HistoryError, "get callback not configured" unless @get
83
-
84
- @get.call(session_id: session_id, **kwargs)
85
- end
86
-
87
- # Append a user message to the thread
88
- #
89
- # @param session_id [String] Thread identifier
90
- # @param message [UserMessage] Message to append
91
- # @param kwargs [Hash] Additional arguments
92
- #
93
- def append_user_message!(session_id:, message:, **kwargs)
94
- return unless @append_user_message
95
-
96
- @append_user_message.call(session_id: session_id, message: message, **kwargs)
97
- end
98
-
99
- # Append robot results to the thread
100
- #
101
- # @param session_id [String] Thread identifier
102
- # @param new_results [Array<RobotResult>] Results to append
103
- # @param kwargs [Hash] Additional arguments
104
- #
105
- def append_results!(session_id:, new_results:, **kwargs)
106
- return unless @append_results
107
-
108
- @append_results.call(session_id: session_id, new_results: new_results, **kwargs)
109
- end
110
- end
111
-
112
- # Error raised when history operations fail
113
- class HistoryError < RobotLab::Error; end
114
- end
115
- end
@@ -1,93 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RobotLab
4
- module History
5
- # Manages conversation thread lifecycle
6
- #
7
- # Handles thread creation, history retrieval, and result persistence
8
- # using the configured history adapter.
9
- #
10
- # @example
11
- # manager = ThreadManager.new(config)
12
- # session_id = manager.create_thread(state: state, input: "Hello")
13
- # history = manager.get_history(session_id)
14
- #
15
- class ThreadManager
16
- # @!attribute [r] config
17
- # @return [Config] the history configuration
18
- attr_reader :config
19
-
20
- # Initialize thread manager
21
- #
22
- # @param config [Config] History configuration
23
- #
24
- def initialize(config)
25
- @config = config
26
- end
27
-
28
- # Create a new conversation thread
29
- #
30
- # @param state [State] Current state
31
- # @param input [String, UserMessage] Initial input
32
- # @return [String] Thread ID
33
- #
34
- def create_thread(state:, input:)
35
- result = @config.create_thread!(state: state, input: input)
36
- result[:session_id]
37
- end
38
-
39
- # Get history for a thread
40
- #
41
- # @param session_id [String] Thread identifier
42
- # @return [Array<RobotResult>] History of results
43
- #
44
- def get_history(session_id)
45
- @config.get!(session_id: session_id)
46
- end
47
-
48
- # Append user message to thread
49
- #
50
- # @param session_id [String] Thread identifier
51
- # @param message [UserMessage] Message to append
52
- #
53
- def append_user_message(session_id:, message:)
54
- @config.append_user_message!(session_id: session_id, message: message)
55
- end
56
-
57
- # Append results to thread
58
- #
59
- # @param session_id [String] Thread identifier
60
- # @param results [Array<RobotResult>] Results to append
61
- #
62
- def append_results(session_id:, results:)
63
- @config.append_results!(session_id: session_id, new_results: results)
64
- end
65
-
66
- # Load state from thread history
67
- #
68
- # @param session_id [String] Thread identifier
69
- # @param state [State, Memory] State/Memory to populate
70
- # @return [State, Memory] State/Memory with loaded history
71
- #
72
- def load_state(session_id:, state:)
73
- results = get_history(session_id)
74
-
75
- state.session_id = session_id
76
- results.each { |r| state.append_result(r) }
77
-
78
- state
79
- end
80
-
81
- # Save state results to thread
82
- #
83
- # @param session_id [String] Thread identifier
84
- # @param state [State] State with results to save
85
- # @param since_index [Integer] Save results from this index
86
- #
87
- def save_state(session_id:, state:, since_index: 0)
88
- new_results = state.results[since_index..]
89
- append_results(session_id: session_id, results: new_results) if new_results.any?
90
- end
91
- end
92
- end
93
- end