rails_ai 0.1.0

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 (82) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec_status +96 -0
  3. data/AGENT_GUIDE.md +513 -0
  4. data/Appraisals +49 -0
  5. data/COMMERCIAL_LICENSE_TEMPLATE.md +92 -0
  6. data/FEATURES.md +204 -0
  7. data/LEGAL_PROTECTION_GUIDE.md +222 -0
  8. data/LICENSE +62 -0
  9. data/LICENSE_SUMMARY.md +74 -0
  10. data/MIT-LICENSE +62 -0
  11. data/PERFORMANCE.md +300 -0
  12. data/PROVIDERS.md +495 -0
  13. data/README.md +454 -0
  14. data/Rakefile +11 -0
  15. data/SPEED_OPTIMIZATIONS.md +217 -0
  16. data/STRUCTURE.md +139 -0
  17. data/USAGE_GUIDE.md +288 -0
  18. data/app/channels/ai_stream_channel.rb +33 -0
  19. data/app/components/ai/prompt_component.rb +25 -0
  20. data/app/controllers/concerns/ai/context_aware.rb +77 -0
  21. data/app/controllers/concerns/ai/streaming.rb +41 -0
  22. data/app/helpers/ai_helper.rb +164 -0
  23. data/app/jobs/ai/generate_embedding_job.rb +25 -0
  24. data/app/jobs/ai/generate_summary_job.rb +25 -0
  25. data/app/models/concerns/ai/embeddable.rb +38 -0
  26. data/app/views/rails_ai/dashboard/index.html.erb +51 -0
  27. data/config/routes.rb +19 -0
  28. data/lib/generators/rails_ai/install/install_generator.rb +38 -0
  29. data/lib/rails_ai/agents/agent_manager.rb +258 -0
  30. data/lib/rails_ai/agents/agent_team.rb +243 -0
  31. data/lib/rails_ai/agents/base_agent.rb +331 -0
  32. data/lib/rails_ai/agents/collaboration.rb +238 -0
  33. data/lib/rails_ai/agents/memory.rb +116 -0
  34. data/lib/rails_ai/agents/message_bus.rb +95 -0
  35. data/lib/rails_ai/agents/specialized_agents.rb +391 -0
  36. data/lib/rails_ai/agents/task_queue.rb +111 -0
  37. data/lib/rails_ai/cache.rb +14 -0
  38. data/lib/rails_ai/config.rb +40 -0
  39. data/lib/rails_ai/context.rb +7 -0
  40. data/lib/rails_ai/context_analyzer.rb +86 -0
  41. data/lib/rails_ai/engine.rb +48 -0
  42. data/lib/rails_ai/events.rb +9 -0
  43. data/lib/rails_ai/image_context.rb +110 -0
  44. data/lib/rails_ai/performance.rb +231 -0
  45. data/lib/rails_ai/provider.rb +8 -0
  46. data/lib/rails_ai/providers/anthropic_adapter.rb +256 -0
  47. data/lib/rails_ai/providers/base.rb +60 -0
  48. data/lib/rails_ai/providers/dummy_adapter.rb +29 -0
  49. data/lib/rails_ai/providers/gemini_adapter.rb +509 -0
  50. data/lib/rails_ai/providers/openai_adapter.rb +535 -0
  51. data/lib/rails_ai/providers/secure_anthropic_adapter.rb +206 -0
  52. data/lib/rails_ai/providers/secure_openai_adapter.rb +284 -0
  53. data/lib/rails_ai/railtie.rb +48 -0
  54. data/lib/rails_ai/redactor.rb +12 -0
  55. data/lib/rails_ai/security/api_key_manager.rb +82 -0
  56. data/lib/rails_ai/security/audit_logger.rb +46 -0
  57. data/lib/rails_ai/security/error_handler.rb +62 -0
  58. data/lib/rails_ai/security/input_validator.rb +176 -0
  59. data/lib/rails_ai/security/secure_file_handler.rb +45 -0
  60. data/lib/rails_ai/security/secure_http_client.rb +177 -0
  61. data/lib/rails_ai/security.rb +0 -0
  62. data/lib/rails_ai/version.rb +5 -0
  63. data/lib/rails_ai/window_context.rb +103 -0
  64. data/lib/rails_ai.rb +502 -0
  65. data/monitoring/ci_setup_guide.md +214 -0
  66. data/monitoring/enhanced_monitoring_script.rb +237 -0
  67. data/monitoring/google_alerts_setup.md +42 -0
  68. data/monitoring_log_20250921.txt +0 -0
  69. data/monitoring_script.rb +161 -0
  70. data/rails_ai.gemspec +54 -0
  71. data/scripts/security_scanner.rb +353 -0
  72. data/setup_monitoring.sh +163 -0
  73. data/wiki/API-Documentation.md +734 -0
  74. data/wiki/Architecture-Overview.md +672 -0
  75. data/wiki/Contributing-Guide.md +407 -0
  76. data/wiki/Development-Setup.md +532 -0
  77. data/wiki/Home.md +278 -0
  78. data/wiki/Installation-Guide.md +527 -0
  79. data/wiki/Quick-Start.md +186 -0
  80. data/wiki/README.md +135 -0
  81. data/wiki/Release-Process.md +467 -0
  82. metadata +385 -0
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAi
4
+ module Agents
5
+ class AgentTeam
6
+ attr_reader :name, :agents, :strategy, :manager, :created_at
7
+
8
+ def initialize(name:, agents:, strategy: :round_robin, manager:)
9
+ @name = name
10
+ @agents = Array(agents)
11
+ @strategy = strategy
12
+ @manager = manager
13
+ @created_at = Time.now
14
+ @current_agent_index = 0
15
+ @team_memory = {}
16
+ @collaboration_history = []
17
+ end
18
+
19
+ # Team task assignment
20
+ def assign_task(task)
21
+ case @strategy
22
+ when :round_robin
23
+ assign_round_robin(task)
24
+ when :capability_based
25
+ assign_capability_based(task)
26
+ when :load_balanced
27
+ assign_load_balanced(task)
28
+ when :collaborative
29
+ assign_collaborative(task)
30
+ else
31
+ assign_round_robin(task)
32
+ end
33
+ end
34
+
35
+ def collaborate_on_task(task)
36
+ return false unless @strategy == :collaborative
37
+
38
+ collaboration = {
39
+ id: SecureRandom.uuid,
40
+ task: task,
41
+ agents: @agents.map(&:name),
42
+ started_at: Time.now,
43
+ status: :in_progress,
44
+ contributions: {}
45
+ }
46
+
47
+ # Each agent contributes to the task
48
+ @agents.each do |agent|
49
+ begin
50
+ contribution = agent.collaborate_with(agent, task, context: build_team_context)
51
+ collaboration[:contributions][agent.name] = contribution
52
+ rescue => e
53
+ defined?(Rails) && Rails.logger && Rails.logger.error("Agent #{agent.name} collaboration failed: #{e.message}")
54
+ collaboration[:contributions][agent.name] = { error: e.message }
55
+ end
56
+ end
57
+
58
+ collaboration[:completed_at] = Time.now
59
+ collaboration[:status] = :completed
60
+ @collaboration_history << collaboration
61
+
62
+ defined?(Rails) && Rails.logger && Rails.logger.info("Team #{@name} completed collaborative task: #{task[:description]}")
63
+ collaboration
64
+ end
65
+
66
+ # Team communication
67
+ def team_meeting(agenda)
68
+ meeting = {
69
+ id: SecureRandom.uuid,
70
+ agenda: agenda,
71
+ participants: @agents.map(&:name),
72
+ started_at: Time.now,
73
+ discussions: {}
74
+ }
75
+
76
+ # Each agent shares their perspective
77
+ @agents.each do |agent|
78
+ perspective = agent.think("Team meeting agenda: #{agenda}. Share your perspective and insights.",
79
+ context: build_team_context)
80
+ meeting[:discussions][agent.name] = perspective
81
+ end
82
+
83
+ meeting[:ended_at] = Time.now
84
+ meeting[:duration] = meeting[:ended_at] - meeting[:started_at]
85
+
86
+ # Store meeting results in team memory
87
+ @team_memory["meeting_#{meeting[:id]}"] = meeting
88
+
89
+ defined?(Rails) && Rails.logger && Rails.logger.info("Team #{@name} meeting completed: #{agenda}")
90
+ meeting
91
+ end
92
+
93
+ def share_knowledge(agent_name, knowledge)
94
+ @team_memory["knowledge_#{Time.now.to_i}"] = {
95
+ shared_by: agent_name,
96
+ knowledge: knowledge,
97
+ shared_at: Time.now
98
+ }
99
+
100
+ # Broadcast knowledge to other agents
101
+ @manager.broadcast_message(agent_name, {
102
+ type: :knowledge_share,
103
+ knowledge: knowledge,
104
+ shared_by: agent_name
105
+ }, exclude: [agent_name])
106
+
107
+ defined?(Rails) && Rails.logger && Rails.logger.info("Agent #{agent_name} shared knowledge with team #{@name}")
108
+ end
109
+
110
+ # Team monitoring
111
+ def team_status
112
+ {
113
+ name: @name,
114
+ strategy: @strategy,
115
+ agent_count: @agents.length,
116
+ active_agents: @agents.count { |a| a.state == :active },
117
+ total_tasks: @agents.sum { |a| a.active_tasks.length },
118
+ team_memory_size: @team_memory.length,
119
+ collaboration_count: @collaboration_history.length,
120
+ created_at: @created_at
121
+ }
122
+ end
123
+
124
+ def team_health
125
+ agent_health = @agents.map do |agent|
126
+ { name: agent.name, health: agent.health_check }
127
+ end
128
+
129
+ {
130
+ overall_health: agent_health.all? { |ah| ah[:health].values.all? },
131
+ agent_health: agent_health,
132
+ memory_usage: calculate_team_memory_usage,
133
+ collaboration_success_rate: calculate_collaboration_success_rate
134
+ }
135
+ end
136
+
137
+ # Team learning
138
+ def learn_from_experience
139
+ recent_collaborations = @collaboration_history.last(10)
140
+ return if recent_collaborations.empty?
141
+
142
+ insights = analyze_collaboration_patterns(recent_collaborations)
143
+
144
+ # Share insights with all agents
145
+ @agents.each do |agent|
146
+ agent.remember("team_insights_#{Time.now.to_i}", insights, importance: :high)
147
+ end
148
+
149
+ defined?(Rails) && Rails.logger && Rails.logger.info("Team #{@name} learned from recent collaborations")
150
+ insights
151
+ end
152
+
153
+ private
154
+
155
+ def assign_round_robin(task)
156
+ agent = @agents[@current_agent_index]
157
+ @current_agent_index = (@current_agent_index + 1) % @agents.length
158
+
159
+ agent.assign_task(task)
160
+ end
161
+
162
+ def assign_capability_based(task)
163
+ required_capabilities = task[:required_capabilities] || []
164
+ return false if required_capabilities.empty?
165
+
166
+ best_agent = @agents
167
+ .select { |agent| agent.state == :active }
168
+ .max_by do |agent|
169
+ required_capabilities.count { |cap| agent.has_capability?(cap) }
170
+ end
171
+
172
+ best_agent&.assign_task(task)
173
+ end
174
+
175
+ def assign_load_balanced(task)
176
+ best_agent = @agents
177
+ .select { |agent| agent.state == :active }
178
+ .min_by { |agent| agent.active_tasks.length }
179
+
180
+ best_agent&.assign_task(task)
181
+ end
182
+
183
+ def assign_collaborative(task)
184
+ # For collaborative tasks, all agents work together
185
+ collaborate_on_task(task)
186
+ end
187
+
188
+ def build_team_context
189
+ {
190
+ team_name: @name,
191
+ team_strategy: @strategy,
192
+ team_members: @agents.map { |a| { name: a.name, role: a.role, capabilities: a.capabilities } },
193
+ team_memory: @team_memory,
194
+ recent_collaborations: @collaboration_history.last(5)
195
+ }
196
+ end
197
+
198
+ def calculate_team_memory_usage
199
+ total_memory = @agents.sum { |agent| agent.memory.size }
200
+ total_capacity = @agents.sum { |agent| agent.memory.instance_variable_get(:@max_size) }
201
+
202
+ return 0 if total_capacity.zero?
203
+
204
+ (total_memory.to_f / total_capacity * 100).round(2)
205
+ end
206
+
207
+ def calculate_collaboration_success_rate
208
+ return 0 if @collaboration_history.empty?
209
+
210
+ successful = @collaboration_history.count { |c| c[:status] == :completed }
211
+ (successful.to_f / @collaboration_history.length * 100).round(2)
212
+ end
213
+
214
+ def analyze_collaboration_patterns(collaborations)
215
+ {
216
+ total_collaborations: collaborations.length,
217
+ success_rate: calculate_collaboration_success_rate,
218
+ average_contributors: collaborations.map { |c| c[:contributions].length }.sum.to_f / collaborations.length,
219
+ common_issues: extract_common_issues(collaborations),
220
+ best_practices: extract_best_practices(collaborations)
221
+ }
222
+ end
223
+
224
+ def extract_common_issues(collaborations)
225
+ issues = collaborations.flat_map do |collab|
226
+ collab[:contributions].values.select { |c| c[:error] }.map { |c| c[:error] }
227
+ end
228
+ issues.tally.sort_by { |_, count| -count }.first(3)
229
+ end
230
+
231
+ def extract_best_practices(collaborations)
232
+ successful_collaborations = collaborations.select { |c| c[:status] == :completed }
233
+ return [] if successful_collaborations.empty?
234
+
235
+ # Analyze what made successful collaborations work
236
+ {
237
+ average_duration: successful_collaborations.map { |c| c[:completed_at] - c[:started_at] }.sum / successful_collaborations.length,
238
+ most_active_contributors: successful_collaborations.flat_map { |c| c[:contributions].keys }.tally.sort_by { |_, count| -count }.first(3)
239
+ }
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,331 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAi
4
+ module Agents
5
+ class BaseAgent
6
+ attr_reader :name, :role, :capabilities, :memory, :state, :created_at, :last_activity, :active_tasks, :completed_tasks, :failed_tasks
7
+
8
+ def initialize(name:, role:, capabilities: [], memory_size: 1000, **opts)
9
+ @name = name
10
+ @role = role
11
+ @capabilities = Array(capabilities)
12
+ @memory = Memory.new(max_size: memory_size)
13
+ @state = :idle
14
+ @created_at = Time.now
15
+ @last_activity = Time.now
16
+ @config = opts
17
+ @message_queue = []
18
+ @active_tasks = []
19
+ @completed_tasks = []
20
+ @failed_tasks = []
21
+ end
22
+
23
+ # Core agent lifecycle methods
24
+ def start!
25
+ @state = :active
26
+ @last_activity = Time.now
27
+ defined?(Rails) && Rails.logger && Rails.logger.info("Agent #{@name} started")
28
+ self
29
+ end
30
+
31
+ def stop!
32
+ @state = :stopped
33
+ @last_activity = Time.now
34
+ defined?(Rails) && Rails.logger && Rails.logger.info("Agent #{@name} stopped")
35
+ self
36
+ end
37
+
38
+ def pause!
39
+ @state = :paused
40
+ @last_activity = Time.now
41
+ defined?(Rails) && Rails.logger && Rails.logger.info("Agent #{@name} paused")
42
+ self
43
+ end
44
+
45
+ def resume!
46
+ @state = :active
47
+ @last_activity = Time.now
48
+ defined?(Rails) && Rails.logger && Rails.logger.info("Agent #{@name} resumed")
49
+ self
50
+ end
51
+
52
+ # Task management
53
+ def assign_task(task)
54
+ return false unless can_handle_task?(task)
55
+
56
+ @active_tasks << task.merge(
57
+ assigned_at: Time.now,
58
+ status: :in_progress
59
+ )
60
+ @last_activity = Time.now
61
+
62
+ defined?(Rails) && Rails.logger && Rails.logger.info("Agent #{@name} assigned task: #{task[:description]}")
63
+ true
64
+ end
65
+
66
+ def complete_task(task_id, result)
67
+ task = @active_tasks.find { |t| t[:id] == task_id }
68
+ return false unless task
69
+
70
+ @active_tasks.delete(task)
71
+ @completed_tasks << task.merge(
72
+ completed_at: Time.now,
73
+ status: :completed,
74
+ result: result
75
+ )
76
+ @last_activity = Time.now
77
+
78
+ defined?(Rails) && Rails.logger && Rails.logger.info("Agent #{@name} completed task: #{task[:description]}")
79
+ true
80
+ end
81
+
82
+ def fail_task(task_id, error)
83
+ task = @active_tasks.find { |t| t[:id] == task_id }
84
+ return false unless task
85
+
86
+ @active_tasks.delete(task)
87
+ @failed_tasks << task.merge(
88
+ failed_at: Time.now,
89
+ status: :failed,
90
+ error: error
91
+ )
92
+ @last_activity = Time.now
93
+
94
+ defined?(Rails) && Rails.logger && Rails.logger.error("Agent #{@name} failed task: #{task[:description]} - #{error}")
95
+ true
96
+ end
97
+
98
+ # Communication methods
99
+ def send_message(to_agent, message)
100
+ message_obj = {
101
+ from: @name,
102
+ to: to_agent,
103
+ content: message,
104
+ timestamp: Time.now,
105
+ id: SecureRandom.uuid
106
+ }
107
+
108
+ @message_queue << message_obj
109
+ @last_activity = Time.now
110
+
111
+ defined?(Rails) && Rails.logger && Rails.logger.info("Agent #{@name} sent message to #{to_agent}")
112
+ message_obj
113
+ end
114
+
115
+ def receive_message(message)
116
+ @memory.add(:message, message)
117
+ @last_activity = Time.now
118
+ defined?(Rails) && Rails.logger && Rails.logger.info("Agent #{@name} received message from #{message[:from]}")
119
+ end
120
+
121
+ def get_messages(from_agent = nil)
122
+ messages = @message_queue
123
+ messages = messages.select { |m| m[:from] == from_agent } if from_agent
124
+ messages
125
+ end
126
+
127
+ # AI-powered decision making
128
+ def think(prompt, context: {})
129
+ return "[stubbed] Agent #{@name} thinking: #{prompt}" if RailsAi.config.stub_responses
130
+
131
+ full_context = build_context(context)
132
+ enhanced_prompt = build_agent_prompt(prompt, full_context)
133
+
134
+ RailsAi.chat(enhanced_prompt, model: RailsAi.config.default_model)
135
+ end
136
+
137
+ def decide_next_action(context: {})
138
+ return { action: :wait, reason: "Stubbed response" } if RailsAi.config.stub_responses
139
+
140
+ decision_prompt = build_decision_prompt(context)
141
+ response = think(decision_prompt, context: context)
142
+
143
+ parse_decision(response)
144
+ end
145
+
146
+ def collaborate_with(other_agent, task, context: {})
147
+ return "[stubbed] Collaboration between #{@name} and #{other_agent.name}" if RailsAi.config.stub_responses
148
+
149
+ collaboration_prompt = build_collaboration_prompt(other_agent, task, context)
150
+ response = think(collaboration_prompt, context: context)
151
+
152
+ # Send collaboration result to other agent
153
+ send_message(other_agent.name, {
154
+ type: :collaboration_result,
155
+ task: task,
156
+ result: response,
157
+ from_agent: @name
158
+ })
159
+
160
+ response
161
+ end
162
+
163
+ # Capability checks
164
+ def can_handle_task?(task)
165
+ return false unless @state == :active
166
+ return false if @active_tasks.length >= max_concurrent_tasks
167
+
168
+ required_capabilities = task[:required_capabilities] || []
169
+ required_capabilities.all? { |cap| @capabilities.include?(cap) }
170
+ end
171
+
172
+ def has_capability?(capability)
173
+ @capabilities.include?(capability.to_sym)
174
+ end
175
+
176
+ # Status and monitoring
177
+ def status
178
+ {
179
+ name: @name,
180
+ role: @role,
181
+ state: @state,
182
+ capabilities: @capabilities,
183
+ active_tasks: @active_tasks.length,
184
+ completed_tasks: @completed_tasks.length,
185
+ failed_tasks: @failed_tasks.length,
186
+ memory_usage: @memory.usage_percentage,
187
+ last_activity: @last_activity,
188
+ uptime: Time.now - @created_at
189
+ }
190
+ end
191
+
192
+ def health_check
193
+ {
194
+ state: @state,
195
+ memory_healthy: @memory.usage_percentage < 90,
196
+ no_stuck_tasks: @active_tasks.none? { |t| Time.now - t[:assigned_at] > max_task_duration },
197
+ last_activity_recent: Time.now - @last_activity < 5 * 60
198
+ }
199
+ end
200
+
201
+ # Memory management
202
+ def remember(key, value, importance: :normal)
203
+ @memory.add(key, value, importance: importance)
204
+ @last_activity = Time.now
205
+ end
206
+
207
+ def recall(key)
208
+ @memory.get(key)
209
+ end
210
+
211
+ def forget(key)
212
+ @memory.remove(key)
213
+ @last_activity = Time.now
214
+ end
215
+
216
+ # Task delegation
217
+ def delegate_task(task, target_agent, reason: nil)
218
+ return false unless can_delegate_task?(task, target_agent)
219
+
220
+ delegation = {
221
+ task: task,
222
+ from_agent: @name,
223
+ to_agent: target_agent.name,
224
+ reason: reason,
225
+ delegated_at: Time.now,
226
+ status: :pending
227
+ }
228
+
229
+ send_message(target_agent.name, {
230
+ type: :task_delegation,
231
+ delegation: delegation
232
+ })
233
+
234
+ @last_activity = Time.now
235
+ defined?(Rails) && Rails.logger && Rails.logger.info("Agent #{@name} delegated task to #{target_agent.name}")
236
+ delegation
237
+ end
238
+
239
+ def accept_delegated_task(delegation)
240
+ task = delegation[:delegation][:task]
241
+ return false unless can_handle_task?(task)
242
+
243
+ assign_task(task.merge(
244
+ delegated_from: delegation[:from],
245
+ delegation_id: delegation[:delegation][:id]
246
+ ))
247
+ end
248
+
249
+ private
250
+
251
+ def max_concurrent_tasks
252
+ @config[:max_concurrent_tasks] || 3
253
+ end
254
+
255
+ def max_task_duration
256
+ @config[:max_task_duration] || 30.minutes
257
+ end
258
+
259
+ def can_delegate_task?(task, target_agent)
260
+ target_agent.can_handle_task?(task) &&
261
+ target_agent.state == :active &&
262
+ @state == :active
263
+ end
264
+
265
+ def build_context(context)
266
+ {
267
+ agent_name: @name,
268
+ agent_role: @role,
269
+ agent_capabilities: @capabilities,
270
+ current_tasks: @active_tasks,
271
+ recent_memory: @memory.recent(10),
272
+ current_time: Time.now,
273
+ **context
274
+ }
275
+ end
276
+
277
+ def build_agent_prompt(prompt, context)
278
+ <<~PROMPT
279
+ You are #{@name}, a #{@role} AI agent with the following capabilities: #{@capabilities.join(', ')}.
280
+
281
+ Current context:
282
+ #{context.map { |k, v| "#{k}: #{v}" }.join("\n")}
283
+
284
+ Your task: #{prompt}
285
+
286
+ Respond as this agent would, considering your role and capabilities.
287
+ PROMPT
288
+ end
289
+
290
+ def build_decision_prompt(context)
291
+ <<~PROMPT
292
+ As #{@name}, analyze the current situation and decide what to do next.
293
+
294
+ Current context:
295
+ #{context.map { |k, v| "#{k}: #{v}" }.join("\n")}
296
+
297
+ Available actions:
298
+ - :wait (if no immediate action needed)
299
+ - :think (if need more information)
300
+ - :act (if ready to take action)
301
+ - :collaborate (if need help from other agents)
302
+ - :delegate (if task should be handled by another agent)
303
+
304
+ Respond with a JSON object: {"action": "action_name", "reason": "explanation", "details": {...}}
305
+ PROMPT
306
+ end
307
+
308
+ def build_collaboration_prompt(other_agent, task, context)
309
+ <<~PROMPT
310
+ You are #{@name} collaborating with #{other_agent.name} (#{other_agent.role}).
311
+
312
+ Task: #{task[:description]}
313
+ Other agent's capabilities: #{other_agent.capabilities.join(', ')}
314
+
315
+ Context:
316
+ #{context.map { |k, v| "#{k}: #{v}" }.join("\n")}
317
+
318
+ Provide your contribution to this collaboration.
319
+ PROMPT
320
+ end
321
+
322
+ def parse_decision(response)
323
+ begin
324
+ JSON.parse(response)
325
+ rescue JSON::ParserError
326
+ { action: :wait, reason: "Failed to parse decision", error: response }
327
+ end
328
+ end
329
+ end
330
+ end
331
+ end