robot_lab 0.0.4 → 0.0.7

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -0
  3. data/README.md +64 -6
  4. data/Rakefile +2 -1
  5. data/docs/api/core/index.md +41 -46
  6. data/docs/api/core/memory.md +200 -154
  7. data/docs/api/core/network.md +13 -3
  8. data/docs/api/core/robot.md +38 -26
  9. data/docs/api/core/state.md +55 -73
  10. data/docs/api/index.md +7 -28
  11. data/docs/api/messages/index.md +35 -20
  12. data/docs/api/messages/text-message.md +67 -21
  13. data/docs/api/messages/tool-call-message.md +80 -41
  14. data/docs/api/messages/tool-result-message.md +119 -50
  15. data/docs/api/messages/user-message.md +48 -24
  16. data/docs/architecture/core-concepts.md +10 -15
  17. data/docs/concepts.md +5 -7
  18. data/docs/examples/index.md +2 -2
  19. data/docs/getting-started/configuration.md +80 -0
  20. data/docs/guides/building-robots.md +10 -9
  21. data/docs/guides/creating-networks.md +49 -0
  22. data/docs/guides/index.md +0 -5
  23. data/docs/guides/rails-integration.md +244 -162
  24. data/docs/guides/streaming.md +118 -138
  25. data/docs/index.md +0 -8
  26. data/examples/03_network.rb +10 -7
  27. data/examples/08_llm_config.rb +40 -11
  28. data/examples/09_chaining.rb +45 -6
  29. data/examples/11_network_introspection.rb +30 -7
  30. data/examples/12_message_bus.rb +1 -1
  31. data/examples/14_rusty_circuit/heckler.rb +14 -8
  32. data/examples/14_rusty_circuit/open_mic.rb +5 -3
  33. data/examples/14_rusty_circuit/scout.rb +14 -31
  34. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +1 -1
  35. data/examples/16_writers_room/display.rb +158 -0
  36. data/examples/16_writers_room/output/.gitignore +4 -0
  37. data/examples/16_writers_room/output/README.md +69 -0
  38. data/examples/16_writers_room/output/opus_001.md +263 -0
  39. data/examples/16_writers_room/output/opus_001_notes.log +470 -0
  40. data/examples/16_writers_room/output/opus_002.md +245 -0
  41. data/examples/16_writers_room/output/opus_002_notes.log +546 -0
  42. data/examples/16_writers_room/output/opus_002_screenplay.md +7989 -0
  43. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +993 -0
  44. data/examples/16_writers_room/prompts/screenplay_writer.md +66 -0
  45. data/examples/16_writers_room/prompts/writer.md +37 -0
  46. data/examples/16_writers_room/room.rb +186 -0
  47. data/examples/16_writers_room/tools.rb +173 -0
  48. data/examples/16_writers_room/writer.rb +121 -0
  49. data/examples/16_writers_room/writers_room.rb +256 -0
  50. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -13
  51. data/lib/robot_lab/memory.rb +8 -32
  52. data/lib/robot_lab/network.rb +13 -20
  53. data/lib/robot_lab/robot/bus_messaging.rb +239 -0
  54. data/lib/robot_lab/robot/mcp_management.rb +88 -0
  55. data/lib/robot_lab/robot/template_rendering.rb +130 -0
  56. data/lib/robot_lab/robot.rb +56 -420
  57. data/lib/robot_lab/run_config.rb +184 -0
  58. data/lib/robot_lab/state_proxy.rb +2 -12
  59. data/lib/robot_lab/task.rb +8 -1
  60. data/lib/robot_lab/utils.rb +39 -0
  61. data/lib/robot_lab/version.rb +1 -1
  62. data/lib/robot_lab.rb +29 -8
  63. data/mkdocs.yml +0 -11
  64. metadata +21 -20
  65. data/docs/api/adapters/anthropic.md +0 -121
  66. data/docs/api/adapters/gemini.md +0 -133
  67. data/docs/api/adapters/index.md +0 -104
  68. data/docs/api/adapters/openai.md +0 -134
  69. data/docs/api/history/active-record-adapter.md +0 -275
  70. data/docs/api/history/config.md +0 -284
  71. data/docs/api/history/index.md +0 -128
  72. data/docs/api/history/thread-manager.md +0 -194
  73. data/docs/guides/history.md +0 -359
  74. data/lib/robot_lab/adapters/anthropic.rb +0 -163
  75. data/lib/robot_lab/adapters/base.rb +0 -85
  76. data/lib/robot_lab/adapters/gemini.rb +0 -193
  77. data/lib/robot_lab/adapters/openai.rb +0 -160
  78. data/lib/robot_lab/adapters/registry.rb +0 -81
  79. data/lib/robot_lab/errors.rb +0 -70
  80. data/lib/robot_lab/history/active_record_adapter.rb +0 -146
  81. data/lib/robot_lab/history/config.rb +0 -115
  82. data/lib/robot_lab/history/thread_manager.rb +0 -93
  83. data/lib/robot_lab/robotic_model.rb +0 -324
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ class Robot < RubyLLM::Agent
5
+ # MCP client lifecycle and hierarchical tool/MCP resolution.
6
+ #
7
+ # Expects the including class to provide:
8
+ # @mcp_config, @tools_config, @mcp_clients, @mcp_tools,
9
+ # @mcp_initialized, @name, @chat, @local_tools
10
+ module MCPManagement
11
+ private
12
+
13
+ # Resolve MCP hierarchy: runtime -> robot build -> network -> config
14
+ def resolve_mcp_hierarchy(runtime_value, network: nil, network_config: nil)
15
+ parent_value = network_config&.mcp || network&.network&.mcp || RobotLab.config.mcp
16
+ build_resolved = ToolConfig.resolve_mcp(@mcp_config, parent_value: parent_value)
17
+ ToolConfig.resolve_mcp(runtime_value, parent_value: build_resolved)
18
+ end
19
+
20
+
21
+ # Resolve tools hierarchy: runtime -> robot build -> network -> config
22
+ def resolve_tools_hierarchy(runtime_value, network: nil, network_config: nil)
23
+ parent_value = network_config&.tools || network&.network&.tools || RobotLab.config.tools
24
+ build_resolved = ToolConfig.resolve_tools(@tools_config, parent_value: parent_value)
25
+ ToolConfig.resolve_tools(runtime_value, parent_value: build_resolved)
26
+ end
27
+
28
+
29
+ # Ensure MCP clients are initialized for the given server configs
30
+ def ensure_mcp_clients(mcp_servers)
31
+ return if mcp_servers.empty?
32
+
33
+ needed_servers = mcp_servers.map { |s| s.is_a?(Hash) ? s[:name] : s.to_s }.compact
34
+ return if @mcp_initialized && (@mcp_clients.keys.sort == needed_servers.sort)
35
+
36
+ disconnect if @mcp_initialized
37
+
38
+ @mcp_clients = {}
39
+ @mcp_tools = []
40
+
41
+ mcp_servers.each do |server_config|
42
+ init_mcp_client(server_config)
43
+ end
44
+
45
+ @mcp_initialized = true
46
+ end
47
+
48
+
49
+ def init_mcp_client(server_config)
50
+ client = MCP::Client.new(server_config)
51
+ client.connect
52
+
53
+ if client.connected?
54
+ server_name = client.server.name
55
+ @mcp_clients[server_name] = client
56
+ discover_mcp_tools(client, server_name)
57
+ else
58
+ RobotLab.config.logger.warn(
59
+ "Robot '#{@name}' failed to connect to MCP server: #{server_config[:name] || server_config}"
60
+ )
61
+ end
62
+ end
63
+
64
+
65
+ def discover_mcp_tools(client, server_name)
66
+ tools = client.list_tools
67
+
68
+ tools.each do |tool_def|
69
+ tool_name = tool_def[:name]
70
+ mcp_client = client
71
+
72
+ tool = Tool.create(
73
+ name: tool_name,
74
+ description: tool_def[:description],
75
+ parameters: tool_def[:inputSchema],
76
+ mcp: server_name
77
+ ) { |args| mcp_client.call_tool(tool_name, args) }
78
+
79
+ @mcp_tools << tool
80
+ end
81
+
82
+ RobotLab.config.logger.info(
83
+ "Robot '#{@name}' discovered #{tools.size} tools from MCP server '#{server_name}'"
84
+ )
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ class Robot < RubyLLM::Agent
5
+ # Template loading, rendering, and front-matter extraction.
6
+ #
7
+ # Expects the including class to provide:
8
+ # @chat, @template, @build_context, @name, @name_from_constructor,
9
+ # @description, @local_tools, @mcp_config
10
+ module TemplateRendering
11
+ # Front matter keys that map to chat configuration methods
12
+ FRONT_MATTER_CONFIG_KEYS = %i[
13
+ model temperature top_p top_k max_tokens
14
+ presence_penalty frequency_penalty stop
15
+ ].freeze
16
+
17
+ # Front matter keys for robot identity and capabilities.
18
+ # Note: uses `robot_name` because PM::Metadata reserves `name` for the filename.
19
+ FRONT_MATTER_EXTRA_KEYS = %i[tools mcp robot_name description].freeze
20
+
21
+ # Apply a prompt_manager template to the robot's chat
22
+ #
23
+ # @param template_id [Symbol, String] the template identifier
24
+ # @param context [Hash] variables to pass to the template
25
+ # @return [self]
26
+ def with_template(template_id, **context)
27
+ @template = template_id.to_sym
28
+ @build_context = context
29
+ apply_template_to_chat(context)
30
+ self
31
+ end
32
+
33
+ private
34
+
35
+ # Apply a prompt_manager template to the persistent chat.
36
+ # If required parameters are missing, applies front matter config but
37
+ # defers rendering until run time when all values are available.
38
+ def apply_template_to_chat(context)
39
+ parsed = PM.parse(@template)
40
+
41
+ # Extract extra config from front matter (name, description, tools, mcp)
42
+ apply_front_matter_extras(parsed.metadata)
43
+
44
+ # Extract LLM config from front matter and apply to chat.
45
+ # Front matter is the base; @config (from constructor kwargs) overrides.
46
+ fm_config = RunConfig.from_front_matter(parsed.metadata)
47
+ effective = fm_config.merge(@config)
48
+ effective.apply_to(@chat)
49
+
50
+ # Resolve context (could be a Proc)
51
+ resolved_ctx = resolve_context(context, network: nil)
52
+
53
+ # Render the template body with context
54
+ begin
55
+ rendered = parsed.to_s(**resolved_ctx)
56
+ @chat.with_instructions(rendered)
57
+ rescue ArgumentError => e
58
+ raise unless e.message.start_with?("Missing required parameters:")
59
+
60
+ # Required parameters not yet available; template will be
61
+ # fully rendered at run time via rerender_template.
62
+ end
63
+ end
64
+
65
+
66
+ # Re-render the template with run-time context merged into build-time context.
67
+ # prompt_manager parameters may be required (null) and only available at run time.
68
+ def rerender_template(run_context)
69
+ merged = (@build_context || {}).merge(run_context)
70
+ parsed = PM.parse(@template)
71
+ resolved_ctx = resolve_context(merged, network: nil)
72
+ rendered = parsed.to_s(**resolved_ctx)
73
+ @chat.with_instructions(rendered)
74
+ end
75
+
76
+
77
+ # Extract identity and capability keys from front matter metadata.
78
+ # Constructor-provided values take precedence over frontmatter.
79
+ def apply_front_matter_extras(metadata)
80
+ if metadata.respond_to?(:robot_name) && metadata.robot_name && !@name_from_constructor
81
+ @name = metadata.robot_name.to_s
82
+ end
83
+
84
+ if metadata.respond_to?(:description) && metadata.description && @description.nil?
85
+ @description = metadata.description.to_s
86
+ end
87
+
88
+ if metadata.respond_to?(:tools) && metadata.tools.is_a?(Array) && @local_tools.empty?
89
+ @local_tools = resolve_frontmatter_tools(metadata.tools)
90
+ end
91
+
92
+ if metadata.respond_to?(:mcp) && metadata.mcp.is_a?(Array) && ToolConfig.none_value?(@mcp_config)
93
+ @mcp_config = metadata.mcp.map { |m| m.is_a?(Hash) ? m.transform_keys(&:to_sym) : m }
94
+ end
95
+ end
96
+
97
+
98
+ # Resolve string tool names from frontmatter to Ruby constants.
99
+ # Tool subclasses are instantiated; instances are used as-is.
100
+ # Unresolvable names are skipped with a warning.
101
+ def resolve_frontmatter_tools(tool_names)
102
+ tool_names.filter_map do |name|
103
+ case name
104
+ when String
105
+ begin
106
+ const = Object.const_get(name)
107
+ const.is_a?(Class) && const < RubyLLM::Tool ? const.new : const
108
+ rescue NameError
109
+ RobotLab.config.logger.warn("Robot '#{@name}': tool '#{name}' not found, skipping")
110
+ nil
111
+ end
112
+ when Class
113
+ name.new
114
+ else
115
+ name
116
+ end
117
+ end
118
+ end
119
+
120
+
121
+ def resolve_context(context, network:)
122
+ case context
123
+ when Proc then context.call(network: network)
124
+ when Hash then context
125
+ else {}
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end