self_agency 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +40 -0
  4. data/.irbrc +22 -0
  5. data/CHANGELOG.md +5 -0
  6. data/COMMITS.md +196 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +177 -0
  9. data/Rakefile +8 -0
  10. data/docs/api/configuration.md +85 -0
  11. data/docs/api/errors.md +166 -0
  12. data/docs/api/index.md +37 -0
  13. data/docs/api/self-agency-module.md +198 -0
  14. data/docs/architecture/overview.md +181 -0
  15. data/docs/architecture/security.md +101 -0
  16. data/docs/assets/images/self_agency.gif +0 -0
  17. data/docs/assets/images/self_agency.mp4 +0 -0
  18. data/docs/development/contributing.md +45 -0
  19. data/docs/development/setup.md +81 -0
  20. data/docs/development/testing.md +70 -0
  21. data/docs/examples/autonomous-robots.md +109 -0
  22. data/docs/examples/basic-examples.md +237 -0
  23. data/docs/examples/collaborative-robots.md +98 -0
  24. data/docs/examples/full-workflow.md +100 -0
  25. data/docs/examples/index.md +36 -0
  26. data/docs/getting-started/installation.md +71 -0
  27. data/docs/getting-started/quick-start.md +94 -0
  28. data/docs/guide/configuration.md +113 -0
  29. data/docs/guide/generating-methods.md +146 -0
  30. data/docs/guide/how-to-use.md +144 -0
  31. data/docs/guide/lifecycle-hooks.md +86 -0
  32. data/docs/guide/prompt-templates.md +189 -0
  33. data/docs/guide/saving-methods.md +84 -0
  34. data/docs/guide/scopes.md +74 -0
  35. data/docs/guide/source-inspection.md +96 -0
  36. data/docs/index.md +77 -0
  37. data/examples/01_basic_usage.rb +27 -0
  38. data/examples/02_multiple_methods.rb +43 -0
  39. data/examples/03_scopes.rb +40 -0
  40. data/examples/04_source_inspection.rb +46 -0
  41. data/examples/05_lifecycle_hook.rb +55 -0
  42. data/examples/06_configuration.rb +97 -0
  43. data/examples/07_error_handling.rb +103 -0
  44. data/examples/08_class_context.rb +64 -0
  45. data/examples/09_method_override.rb +52 -0
  46. data/examples/10_full_workflow.rb +118 -0
  47. data/examples/11_collaborative_robots/atlas.rb +31 -0
  48. data/examples/11_collaborative_robots/echo.rb +30 -0
  49. data/examples/11_collaborative_robots/main.rb +190 -0
  50. data/examples/11_collaborative_robots/nova.rb +71 -0
  51. data/examples/11_collaborative_robots/robot.rb +119 -0
  52. data/examples/12_autonomous_robots/analyst.rb +193 -0
  53. data/examples/12_autonomous_robots/collector.rb +78 -0
  54. data/examples/12_autonomous_robots/main.rb +166 -0
  55. data/examples/12_autonomous_robots/planner.rb +125 -0
  56. data/examples/12_autonomous_robots/robot.rb +284 -0
  57. data/examples/generated/from_range_class.rb +3 -0
  58. data/examples/generated/mean_instance.rb +4 -0
  59. data/examples/generated/median_instance.rb +15 -0
  60. data/examples/generated/report_singleton.rb +3 -0
  61. data/examples/generated/standard_deviation_instance.rb +8 -0
  62. data/examples/lib/message_bus.rb +57 -0
  63. data/examples/lib/setup.rb +8 -0
  64. data/lib/self_agency/configuration.rb +76 -0
  65. data/lib/self_agency/errors.rb +35 -0
  66. data/lib/self_agency/generator.rb +47 -0
  67. data/lib/self_agency/prompts/generate/system.txt.erb +15 -0
  68. data/lib/self_agency/prompts/generate/user.txt.erb +13 -0
  69. data/lib/self_agency/prompts/shape/system.txt.erb +26 -0
  70. data/lib/self_agency/prompts/shape/user.txt.erb +10 -0
  71. data/lib/self_agency/sandbox.rb +17 -0
  72. data/lib/self_agency/saver.rb +62 -0
  73. data/lib/self_agency/validator.rb +64 -0
  74. data/lib/self_agency/version.rb +5 -0
  75. data/lib/self_agency.rb +315 -0
  76. data/mkdocs.yml +156 -0
  77. data/sig/self_agency.rbs +4 -0
  78. metadata +163 -0
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # 11_collaborative_robots/main.rb — Collaborative Robots Demo
5
+ #
6
+ # Demonstrates:
7
+ # - Two-layer LLM approach: analyze task -> generate methods via _()
8
+ # - Three robots collaborating through a shared message bus
9
+ # - Pipeline execution: data generation -> analysis -> reporting
10
+ #
11
+ # Requires a running Ollama instance with the configured model.
12
+
13
+ require_relative "robot"
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Create message bus and robots
17
+ # ---------------------------------------------------------------------------
18
+
19
+ puts "=== Collaborative Robots — Weather Data Pipeline ==="
20
+ puts ""
21
+
22
+ bus = MessageBus.new
23
+
24
+ atlas = Robot.new(
25
+ name: "Atlas",
26
+ task: <<~TASK,
27
+ You are a data generator robot. Create exactly two instance methods:
28
+
29
+ 1. Method named 'generate_weather_data' that takes no parameters.
30
+ It must return an Array of 24 Hashes (one per hour, index 0..23).
31
+ Each Hash has these keys (all Symbols):
32
+ :hour => the integer hour (0..23)
33
+ :temperature => a Float computed as 20.0 + 8.0 * Math.sin((hour - 6) * Math::PI / 12.0)
34
+ :humidity => a Float computed as 60.0 + 20.0 * Math.cos((hour - 14) * Math::PI / 12.0)
35
+ :wind_speed => a Float computed as 10.0 + 5.0 * Math.sin((hour * 7) * Math::PI / 24.0)
36
+ Do NOT use random numbers. Use only the deterministic formulas above.
37
+
38
+ 2. Method named 'summarize_raw_data' that takes one parameter (data),
39
+ an Array of Hashes as described above. It returns a Hash with:
40
+ :readings_count => data.size
41
+ :raw_data => data
42
+ :source => "Atlas"
43
+ TASK
44
+ bus: bus
45
+ )
46
+
47
+ puts ""
48
+
49
+ nova = Robot.new(
50
+ name: "Nova",
51
+ task: <<~TASK,
52
+ You are an analyzer robot. Create exactly two instance methods:
53
+
54
+ 1. Method named 'compute_basic_statistics' that takes one parameter (data),
55
+ a Hash with key :raw_data containing an Array of Hashes. Each inner Hash
56
+ has keys :temperature, :humidity, :wind_speed (all Floats).
57
+ Compute and return a Hash with:
58
+ :avg_temp => average of all :temperature values, rounded to 1 decimal
59
+ :min_temp => minimum :temperature value, rounded to 1 decimal
60
+ :max_temp => maximum :temperature value, rounded to 1 decimal
61
+ :avg_humidity => average of all :humidity values, rounded to 1 decimal
62
+ :avg_wind => average of all :wind_speed values, rounded to 1 decimal
63
+ :readings => data[:readings_count]
64
+ :source => data[:source]
65
+ Use .round(1) for all Float results.
66
+
67
+ 2. Method named 'classify_conditions' that takes one parameter (stats),
68
+ a Hash with keys :avg_temp, :avg_humidity, :avg_wind (all Floats).
69
+ Determine classifications:
70
+ - temperature_class: "cold" if avg_temp < 15, "mild" if < 25, else "hot"
71
+ - humidity_class: "dry" if avg_humidity < 40, "comfortable" if < 70, else "humid"
72
+ - wind_class: "calm" if avg_wind < 8, "breezy" if < 15, else "windy"
73
+ Return stats.merge with the three new keys (:temperature_class, :humidity_class,
74
+ :wind_class) added, preserving all existing keys.
75
+ TASK
76
+ bus: bus
77
+ )
78
+
79
+ puts ""
80
+
81
+ echo = Robot.new(
82
+ name: "Echo",
83
+ task: <<~TASK,
84
+ You are a reporter robot. Create exactly one instance method:
85
+
86
+ 1. Method named 'format_weather_report' that takes one parameter (data),
87
+ a Hash with these keys:
88
+ :avg_temp, :min_temp, :max_temp (Floats)
89
+ :avg_humidity, :avg_wind (Floats)
90
+ :temperature_class, :humidity_class, :wind_class (Strings)
91
+ :readings (Integer), :source (String)
92
+ Return a formatted multi-line String report like:
93
+
94
+ "=== Weather Report ===\\n" +
95
+ "Source: \#{data[:source]} | Readings: \#{data[:readings]}\\n" +
96
+ "Temperature: \#{data[:avg_temp]}° (min: \#{data[:min_temp]}°, max: \#{data[:max_temp]}°) [\#{data[:temperature_class]}]\\n" +
97
+ "Humidity: \#{data[:avg_humidity]}% [\#{data[:humidity_class]}]\\n" +
98
+ "Wind: \#{data[:avg_wind]} km/h [\#{data[:wind_class]}]\\n" +
99
+ "======================"
100
+
101
+ Use string interpolation. Return the String, do not print it.
102
+ TASK
103
+ bus: bus
104
+ )
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # Display capabilities summary
108
+ # ---------------------------------------------------------------------------
109
+
110
+ puts ""
111
+ puts "=== Capabilities Summary ==="
112
+ [atlas, nova, echo].each do |robot|
113
+ puts "#{robot.name}: #{robot.capabilities.inspect}"
114
+ end
115
+
116
+ puts ""
117
+ puts "=== Generated Source Code ==="
118
+ [atlas, nova, echo].each do |robot|
119
+ robot.generation_log.each do |entry|
120
+ puts "--- #{robot.name}##{entry[:method_name]} ---"
121
+ puts entry[:code]
122
+ puts ""
123
+ end
124
+ end
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Execute the pipeline
128
+ # ---------------------------------------------------------------------------
129
+
130
+ puts "=== Executing Pipeline ==="
131
+ puts ""
132
+
133
+ # Step 1: Atlas generates and summarizes weather data
134
+ puts "Step 1: Atlas generates weather data..."
135
+ atlas_result = atlas.execute
136
+ atlas.send_message(to: "Nova", content: atlas_result)
137
+ puts "Atlas produced #{atlas_result[:raw_data]&.size || 0} readings"
138
+ puts ""
139
+
140
+ # Step 2: Nova analyzes the data
141
+ puts "Step 2: Nova analyzes the data..."
142
+ nova_input = nova.inbox.last&.dig(:content)
143
+ nova_result = nova.execute(nova_input)
144
+ nova.send_message(to: "Echo", content: nova_result)
145
+ puts "Nova computed statistics and classifications"
146
+ puts ""
147
+
148
+ # Step 3: Echo formats the report
149
+ puts "Step 3: Echo formats the final report..."
150
+ echo_input = echo.inbox.last&.dig(:content)
151
+ final_report = echo.execute(echo_input)
152
+ puts ""
153
+
154
+ # ---------------------------------------------------------------------------
155
+ # Display results
156
+ # ---------------------------------------------------------------------------
157
+
158
+ puts "=== Final Weather Report ==="
159
+ puts final_report.to_s
160
+ puts ""
161
+
162
+ bus.print_log
163
+ puts ""
164
+
165
+ # ---------------------------------------------------------------------------
166
+ # Generation statistics
167
+ # ---------------------------------------------------------------------------
168
+
169
+ puts "=== Generation Statistics ==="
170
+ total_methods = 0
171
+ total_lines = 0
172
+ [atlas, nova, echo].each do |robot|
173
+ methods = robot.generation_log.size
174
+ lines = robot.generation_log.sum { |e| e[:code].lines.size }
175
+ total_methods += methods
176
+ total_lines += lines
177
+ puts "#{robot.name}: #{methods} method(s), #{lines} lines of generated code"
178
+ end
179
+ puts "Total: #{total_methods} methods, #{total_lines} lines of generated code"
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # Save generated robots as subclasses
183
+ # ---------------------------------------------------------------------------
184
+
185
+ puts ""
186
+ puts "=== Saving Generated Robots ==="
187
+ [atlas, nova, echo].each do |robot|
188
+ path = robot._save!(as: robot.name)
189
+ puts "#{robot.name}: saved to #{path}"
190
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "robot"
4
+
5
+ class Nova < Robot
6
+ # Compute basic statistics from raw data. Takes one parameter (data), a Hash with key :raw_data containing an Array of Hashes. Each inner Hash has keys :temperature, :humidity, :wind_speed (all Floats). Compute and return a Hash with: :avg_temp => average of all :temperature values, rounded to 1 decimal, :min_temp => minimum :temperature value, rounded to 1 decimal, :max_temp => maximum :temperature value, rounded to 1 decimal, :avg_humidity => average of all :humidity values, rounded to 1 decimal, :avg_wind => average of all :wind_speed values, rounded to 1 decimal, :readings => data[:readings_count], :source => data[:source]. Use .round(1) for all Float results.
7
+ def compute_statistics(data)
8
+ raw_data = data[:raw_data]
9
+ readings_count = data[:readings_count]
10
+ source = data[:source]
11
+
12
+ temperatures = []
13
+ humidities = []
14
+ wind_speeds = []
15
+
16
+ raw_data.each do |reading|
17
+ temperatures << reading[:temperature]
18
+ humidities << reading[:humidity]
19
+ wind_speeds << reading[:wind_speed]
20
+ end
21
+
22
+ avg_temp = (temperatures.sum / temperatures.length.to_f).round(1)
23
+ min_temp = temperatures.min.round(1)
24
+ max_temp = temperatures.max.round(1)
25
+ avg_humidity = (humidities.sum / humidities.length.to_f).round(1)
26
+ avg_wind = (wind_speeds.sum / wind_speeds.length.to_f).round(1)
27
+
28
+ {
29
+ :avg_temp => avg_temp,
30
+ :min_temp => min_temp,
31
+ :max_temp => max_temp,
32
+ :avg_humidity => avg_humidity,
33
+ :avg_wind => avg_wind,
34
+ :readings => readings_count,
35
+ :source => source
36
+ }
37
+ end
38
+
39
+ # Classify weather conditions based on statistics. Takes one parameter (stats), a Hash with keys :avg_temp, :avg_humidity, :avg_wind (all Floats). Determine classifications: - temperature_class: "cold" if avg_temp < 15, "mild" if < 25, else "hot" - humidity_class: "dry" if avg_humidity < 40, "comfortable" if < 70, else "humid" - wind_class: "calm" if avg_wind < 8, "breezy" if < 15, else "windy". Return stats.merge with the three new keys (:temperature_class, :humidity_class, :wind_class) added, preserving all existing keys.
40
+ def classify_weather_conditions(stats)
41
+ temperature_class = if stats[:avg_temp] < 15.0
42
+ "cold"
43
+ elsif stats[:avg_temp] < 25.0
44
+ "mild"
45
+ else
46
+ "hot"
47
+ end
48
+
49
+ humidity_class = if stats[:avg_humidity] < 40.0
50
+ "dry"
51
+ elsif stats[:avg_humidity] < 70.0
52
+ "comfortable"
53
+ else
54
+ "humid"
55
+ end
56
+
57
+ wind_class = if stats[:avg_wind] < 8.0
58
+ "calm"
59
+ elsif stats[:avg_wind] < 15.0
60
+ "breezy"
61
+ else
62
+ "windy"
63
+ end
64
+
65
+ stats.merge(
66
+ temperature_class: temperature_class,
67
+ humidity_class: humidity_class,
68
+ wind_class: wind_class
69
+ )
70
+ end
71
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ # robot.rb — Robot class with two-layer LLM self-generation
4
+ #
5
+ # Layer 1 (Analyze): Direct RubyLLM.chat().ask() decomposes a task into
6
+ # a JSON array of method specifications.
7
+ # Layer 2 (Generate): Loops through specs, calls _() for each.
8
+ # SelfAgency handles shape -> generate -> validate -> sandbox eval.
9
+
10
+ require "json"
11
+ require_relative "../lib/message_bus"
12
+ require_relative "../lib/setup"
13
+
14
+ class Robot
15
+ include SelfAgency
16
+
17
+ attr_reader :name, :task, :bus, :inbox, :capabilities, :generation_log
18
+
19
+ def initialize(name:, task:, bus:)
20
+ @name = name
21
+ @task = task
22
+ @bus = bus
23
+ @inbox = []
24
+ @capabilities = []
25
+ @generation_log = []
26
+
27
+ bus.register(self)
28
+
29
+ puts "#{@name}: Analyzing task..."
30
+ specs = analyze_task(task)
31
+ puts "#{@name}: Found #{specs.size} method(s) to generate"
32
+
33
+ specs.each do |spec|
34
+ method_name = spec["name"]
35
+ description = spec["description"]
36
+ puts "#{@name}: Generating '#{method_name}' — #{description}"
37
+
38
+ begin
39
+ defined_methods = _(description)
40
+ @capabilities.concat(defined_methods)
41
+ puts "#{@name}: Successfully generated #{defined_methods.inspect}"
42
+ rescue SelfAgency::Error => e
43
+ puts "#{@name}: Failed to generate '#{method_name}': #{e.message}"
44
+ end
45
+ end
46
+
47
+ puts "#{@name}: Ready with capabilities: #{@capabilities.inspect}"
48
+ end
49
+
50
+ def execute(input = nil)
51
+ result = nil
52
+
53
+ @capabilities.each do |cap|
54
+ arity = method(cap).arity
55
+
56
+ if arity == 0 && result.nil?
57
+ result = public_send(cap)
58
+ elsif arity != 0 && !result.nil?
59
+ result = public_send(cap, result)
60
+ elsif arity != 0 && result.nil? && !input.nil?
61
+ result = public_send(cap, input)
62
+ end
63
+ end
64
+
65
+ result
66
+ end
67
+
68
+ def receive_message(from:, content:)
69
+ @inbox << { from: from, content: content }
70
+ puts "#{@name}: Received message from #{from}"
71
+ end
72
+
73
+ def send_message(to:, content:)
74
+ @bus.deliver(from: @name, to: to, content: content)
75
+ end
76
+
77
+ def broadcast(content:)
78
+ @bus.broadcast(from: @name, content: content)
79
+ end
80
+
81
+ def on_method_generated(method_name, scope, code)
82
+ @generation_log << { method_name: method_name, scope: scope, code: code }
83
+ end
84
+
85
+ private
86
+
87
+ def analyze_task(description)
88
+ cfg = SelfAgency.configuration
89
+ chat = RubyLLM.chat(model: cfg.model, provider: cfg.provider)
90
+
91
+ prompt = <<~PROMPT
92
+ You are a task decomposition engine. Given a task description, return a JSON
93
+ array of method specifications. Each element must have:
94
+ - "name": the Ruby method name (snake_case), exactly as specified in the task
95
+ - "description": a precise description for a Ruby code generator that
96
+ preserves EVERY identifier verbatim from the task — method names, parameter
97
+ names, Hash key names (as Ruby Symbols), return types, thresholds, and
98
+ formulas. Do NOT paraphrase, rename, or abbreviate any identifier.
99
+ - "takes_input": boolean, true if the method accepts a parameter
100
+
101
+ Respond with ONLY the JSON array. No markdown fences, no explanation.
102
+
103
+ Task: #{description}
104
+ PROMPT
105
+
106
+ response = chat.ask(prompt)
107
+ raw = response.content.to_s.strip
108
+
109
+ # Sanitize the same way SelfAgency does — strip <think> blocks and markdown fences
110
+ raw = raw.gsub(/<think>.*?<\/think>/m, "")
111
+ raw = raw.sub(/\A```\w*\n?/, "").sub(/\n?```\s*\z/, "")
112
+ raw.strip!
113
+
114
+ JSON.parse(raw)
115
+ rescue JSON::ParserError => e
116
+ puts "#{@name}: Failed to parse task analysis: #{e.message}"
117
+ []
118
+ end
119
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "robot"
4
+
5
+ class Analyst < Robot
6
+ # Fix the Ruby singleton method 'calculate_statistics' on this Robot instance.
7
+ #
8
+ # Robot's overall goal: Analyze landmark data. The input is an Array of Hashes, each with keys :name (String), :type (String), :duration (Integer), :rating (Float). Return a Hash with three keys: :statistics (a Hash with :avg_rating, :avg_duration, :total_duration, :count), :ranked (the landmarks Array sorted by :rating descending), :by_type (a Hash grouping landmarks by :type)
9
+ # Generated capabilities on this object: [:calculate_statistics, :sort_by_rating, :group_by_type, :execute_task]
10
+ #
11
+ # Current source code of 'calculate_statistics':
12
+ # def calculate_statistics(landmarks)
13
+ # total_duration = 0
14
+ # rating_sum = 0.0
15
+ #
16
+ # landmarks.each do |landmark|
17
+ # total_duration += landmark[:duration]
18
+ # rating_sum += landmark[:rating]
19
+ # end
20
+ #
21
+ # avg_rating = landmarks.length > 0 ? rating_sum / landmarks.length : 0.0
22
+ # avg_duration = landmarks.length > 0 ? total_duration.to_f / landmarks.length : 0.0
23
+ #
24
+ # {
25
+ # avg_rating: avg_rating,
26
+ # avg_duration: avg_duration,
27
+ # total_duration: total_duration,
28
+ # count: landmarks.length
29
+ # }
30
+ # end
31
+ #
32
+ # Runtime error:
33
+ # NoMethodError: undefined method 'each' for nil
34
+ #
35
+ # Backtrace (top 5):
36
+ # (eval at /Users/dewayne/sandbox/git_repos/madbomber/self_agency/lib/self_agency.rb:265):5:in 'calculate_statistics'
37
+ # (eval at /Users/dewayne/sandbox/git_repos/madbomber/self_agency/lib/self_agency.rb:265):2:in 'execute_task'
38
+ # /Users/dewayne/sandbox/git_repos/madbomber/self_agency/examples/12_autonomous_robots/robot.rb:72:in 'Robot#perform_task'
39
+ # ./main.rb:102:in '<main>'
40
+ #
41
+ # This method takes no external input.
42
+ #
43
+ # Produce a corrected version of this method that avoids the error.
44
+ # Keep the same method name and signature. Only define this one method.
45
+ # Fix the bug while preserving the method's intent.
46
+ def calculate_statistics(landmarks)
47
+ landmarks = landmarks || []
48
+
49
+ total_duration = 0
50
+ rating_sum = 0.0
51
+ count = 0
52
+
53
+ landmarks.each do |landmark|
54
+ next unless landmark.is_a?(Hash)
55
+ next unless landmark[:duration].is_a?(Integer)
56
+ next unless landmark[:rating].is_a?(Float)
57
+
58
+ total_duration += landmark[:duration]
59
+ rating_sum += landmark[:rating]
60
+ count += 1
61
+ end
62
+
63
+ avg_rating = count > 0 ? rating_sum / count : 0.0
64
+ avg_duration = count > 0 ? total_duration.to_f / count : 0.0
65
+
66
+ statistics = {
67
+ :avg_rating => avg_rating,
68
+ :avg_duration => avg_duration,
69
+ :total_duration => total_duration,
70
+ :count => count
71
+ }
72
+
73
+ ranked = landmarks.sort_by { |landmark| -landmark[:rating] }
74
+
75
+ by_type = landmarks.group_by { |landmark| landmark[:type] }
76
+
77
+ {
78
+ :statistics => statistics,
79
+ :ranked => ranked,
80
+ :by_type => by_type
81
+ }
82
+ end
83
+
84
+ # Fix the Ruby singleton method 'sort_by_rating' on this Robot instance.
85
+ #
86
+ # Robot's overall goal: Analyze landmark data. The input is an Array of Hashes, each with keys :name (String), :type (String), :duration (Integer), :rating (Float). Return a Hash with three keys: :statistics (a Hash with :avg_rating, :avg_duration, :total_duration, :count), :ranked (the landmarks Array sorted by :rating descending), :by_type (a Hash grouping landmarks by :type)
87
+ # Generated capabilities on this object: [:calculate_statistics, :sort_by_rating, :group_by_type, :execute_task]
88
+ #
89
+ # Current source code of 'sort_by_rating':
90
+ # def sort_by_rating(landmarks)
91
+ # landmarks.sort_by { |landmark| -landmark[:rating] || 0 }
92
+ # end
93
+ #
94
+ # Runtime error:
95
+ # NoMethodError: undefined method 'sort_by' for nil
96
+ #
97
+ # Backtrace (top 5):
98
+ # (eval at /Users/dewayne/sandbox/git_repos/madbomber/self_agency/lib/self_agency.rb:265):2:in 'sort_by_rating'
99
+ # (eval at /Users/dewayne/sandbox/git_repos/madbomber/self_agency/lib/self_agency.rb:265):3:in 'execute_task'
100
+ # /Users/dewayne/sandbox/git_repos/madbomber/self_agency/examples/12_autonomous_robots/robot.rb:72:in 'Robot#perform_task'
101
+ # ./main.rb:102:in '<main>'
102
+ #
103
+ # This method takes no external input.
104
+ #
105
+ # Produce a corrected version of this method that avoids the error.
106
+ # Keep the same method name and signature. Only define this one method.
107
+ # Fix the bug while preserving the method's intent.
108
+ def sort_by_rating(landmarks)
109
+ return {
110
+ :statistics => {
111
+ :average_rating => 0.0,
112
+ :average_duration => 0.0,
113
+ :total_duration => 0,
114
+ :count => 0
115
+ },
116
+ :ranked => [],
117
+ :by_type => {}
118
+ } if landmarks.nil? || landmarks.empty?
119
+
120
+ total_rating = 0.0
121
+ total_duration = 0
122
+ count = landmarks.length
123
+
124
+ landmarks.each do |landmark|
125
+ total_rating += landmark[:rating] || 0.0
126
+ total_duration += landmark[:duration] || 0
127
+ end
128
+
129
+ average_rating = count > 0 ? total_rating / count : 0.0
130
+ average_duration = count > 0 ? total_duration.to_f / count : 0.0
131
+
132
+ ranked = landmarks.sort_by { |landmark| -(landmark[:rating] || 0.0) }
133
+
134
+ by_type = ranked.group_by { |landmark| landmark[:type] }
135
+
136
+ {
137
+ :statistics => {
138
+ :average_rating => average_rating,
139
+ :average_duration => average_duration,
140
+ :total_duration => total_duration,
141
+ :count => count
142
+ },
143
+ :ranked => ranked,
144
+ :by_type => by_type
145
+ }
146
+ end
147
+
148
+ # def group_by_type(landmarks) -> Hash
149
+ # Groups an array of landmark hashes by their type.
150
+ # Parameters: landmarks (Array[Hash]) - array of landmark data hashes
151
+ # Returns: Hash with type names as keys and Arrays of landmark hashes as values
152
+ # Algorithm:
153
+ # 1. Initialize an empty Hash result
154
+ # 2. Iterate through each landmark in landmarks array
155
+ # 3. For each landmark, get the :type key value
156
+ # 4. If result has a key for this type, append landmark to that array
157
+ # 5. Otherwise, create new array with landmark as first element
158
+ # 6. Return the grouped result hash
159
+ def group_by_type(landmarks)
160
+ result = {}
161
+ landmarks.each do |landmark|
162
+ type = landmark[:type]
163
+ if result.key?(type)
164
+ result[type] << landmark
165
+ else
166
+ result[type] = [landmark]
167
+ end
168
+ end
169
+ result
170
+ end
171
+
172
+ # An instance method named 'execute_task' that orchestrates the following
173
+ # goal: Analyze landmark data. The input is an Array of Hashes, each with keys :name (String), :type (String), :duration (Integer), :rating (Float). Return a Hash with three keys: :statistics (a Hash with :avg_rating, :avg_duration, :total_duration, :count), :ranked (the landmarks Array sorted by :rating descending), :by_type (a Hash grouping landmarks by :type)
174
+ #
175
+ # It takes one parameter (input) which is the data passed in from a previous stage.
176
+ #
177
+ # Available helper methods on this object: calculate_statistics, sort_by_rating, group_by_type
178
+ #
179
+ # Call the helper methods in whatever order makes sense to accomplish the goal.
180
+ # Return the final result. Do NOT define the helper methods — they already exist.
181
+ # Only define the execute_task method itself.
182
+ def execute_task(input)
183
+ statistics = calculate_statistics(input)
184
+ ranked = sort_by_rating(input)
185
+ by_type = group_by_type(input)
186
+
187
+ {
188
+ :statistics => statistics,
189
+ :ranked => ranked,
190
+ :by_type => by_type
191
+ }
192
+ end
193
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "robot"
4
+
5
+ class Collector < Robot
6
+ # def generate_landmark_data
7
+ # landmarks = []
8
+ # 8.times do |i|
9
+ # name = "Landmark #{i + 1}"
10
+ # type = ['museum', 'park', 'bridge', 'tower', 'theater', 'garden', 'statue', 'monument'].sample
11
+ # duration = rand(30..180)
12
+ # rating = (rand(100..500).to_f / 100.0).round(1)
13
+ # landmarks << { name: name, type: type, duration: duration, rating: rating }
14
+ # end
15
+ # landmarks
16
+ # end
17
+ def generate_landmark_data
18
+ landmarks = []
19
+ 8.times do |i|
20
+ name = "Landmark #{i + 1}"
21
+ type = ['museum', 'park', 'bridge', 'tower', 'theater', 'garden', 'statue', 'monument'].sample
22
+ duration = rand(30..180)
23
+ rating = (rand(100..500).to_f / 100.0).round(1)
24
+ landmark = { name: name, type: type, duration: duration, rating: rating }
25
+ landmarks << landmark
26
+ end
27
+ landmarks
28
+ end
29
+
30
+ # Fix the Ruby singleton method 'execute_task' on this Robot instance.
31
+ #
32
+ # Robot's overall goal: Return an Array of 8 Hashes representing fictional city landmarks. Each Hash has Symbol keys :name (String), :type (String, e.g. 'museum'), :duration (Integer, 30..180 minutes), :rating (Float, 1.0..5.0). Do NOT wrap the Array in an outer Hash.
33
+ # Generated capabilities on this object: [:generate_landmark_data, :execute_task]
34
+ #
35
+ # Current source code of 'execute_task':
36
+ # def execute_task
37
+ # landmarks = []
38
+ # 8.times do
39
+ # data = generate_landmark_data
40
+ # landmarks << {
41
+ # name: data[:name],
42
+ # type: data[:type],
43
+ # duration: rand(30..180),
44
+ # rating: rand(1.0..5.0)
45
+ # }
46
+ # end
47
+ # landmarks
48
+ # end
49
+ #
50
+ # Runtime error:
51
+ # TypeError: no implicit conversion of Symbol into Integer
52
+ #
53
+ # Backtrace (top 5):
54
+ # (eval at /Users/dewayne/sandbox/git_repos/madbomber/self_agency/lib/self_agency.rb:265):6:in 'block in execute_task'
55
+ # (eval at /Users/dewayne/sandbox/git_repos/madbomber/self_agency/lib/self_agency.rb:265):3:in 'Integer#times'
56
+ # (eval at /Users/dewayne/sandbox/git_repos/madbomber/self_agency/lib/self_agency.rb:265):3:in 'execute_task'
57
+ # /Users/dewayne/sandbox/git_repos/madbomber/self_agency/examples/12_autonomous_robots/robot.rb:70:in 'Robot#perform_task'
58
+ # ./main.rb:94:in '<main>'
59
+ #
60
+ # This method takes no external input.
61
+ #
62
+ # Produce a corrected version of this method that avoids the error.
63
+ # Keep the same method name and signature. Only define this one method.
64
+ # Fix the bug while preserving the method's intent.
65
+ def execute_task
66
+ landmarks = []
67
+ 8.times do
68
+ data = generate_landmark_data
69
+ landmarks << {
70
+ name: data[:name],
71
+ type: data[:type],
72
+ duration: rand(30..180),
73
+ rating: rand(1.0..5.0)
74
+ }
75
+ end
76
+ landmarks
77
+ end
78
+ end