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.
- checksums.yaml +7 -0
- data/.rspec_status +96 -0
- data/AGENT_GUIDE.md +513 -0
- data/Appraisals +49 -0
- data/COMMERCIAL_LICENSE_TEMPLATE.md +92 -0
- data/FEATURES.md +204 -0
- data/LEGAL_PROTECTION_GUIDE.md +222 -0
- data/LICENSE +62 -0
- data/LICENSE_SUMMARY.md +74 -0
- data/MIT-LICENSE +62 -0
- data/PERFORMANCE.md +300 -0
- data/PROVIDERS.md +495 -0
- data/README.md +454 -0
- data/Rakefile +11 -0
- data/SPEED_OPTIMIZATIONS.md +217 -0
- data/STRUCTURE.md +139 -0
- data/USAGE_GUIDE.md +288 -0
- data/app/channels/ai_stream_channel.rb +33 -0
- data/app/components/ai/prompt_component.rb +25 -0
- data/app/controllers/concerns/ai/context_aware.rb +77 -0
- data/app/controllers/concerns/ai/streaming.rb +41 -0
- data/app/helpers/ai_helper.rb +164 -0
- data/app/jobs/ai/generate_embedding_job.rb +25 -0
- data/app/jobs/ai/generate_summary_job.rb +25 -0
- data/app/models/concerns/ai/embeddable.rb +38 -0
- data/app/views/rails_ai/dashboard/index.html.erb +51 -0
- data/config/routes.rb +19 -0
- data/lib/generators/rails_ai/install/install_generator.rb +38 -0
- data/lib/rails_ai/agents/agent_manager.rb +258 -0
- data/lib/rails_ai/agents/agent_team.rb +243 -0
- data/lib/rails_ai/agents/base_agent.rb +331 -0
- data/lib/rails_ai/agents/collaboration.rb +238 -0
- data/lib/rails_ai/agents/memory.rb +116 -0
- data/lib/rails_ai/agents/message_bus.rb +95 -0
- data/lib/rails_ai/agents/specialized_agents.rb +391 -0
- data/lib/rails_ai/agents/task_queue.rb +111 -0
- data/lib/rails_ai/cache.rb +14 -0
- data/lib/rails_ai/config.rb +40 -0
- data/lib/rails_ai/context.rb +7 -0
- data/lib/rails_ai/context_analyzer.rb +86 -0
- data/lib/rails_ai/engine.rb +48 -0
- data/lib/rails_ai/events.rb +9 -0
- data/lib/rails_ai/image_context.rb +110 -0
- data/lib/rails_ai/performance.rb +231 -0
- data/lib/rails_ai/provider.rb +8 -0
- data/lib/rails_ai/providers/anthropic_adapter.rb +256 -0
- data/lib/rails_ai/providers/base.rb +60 -0
- data/lib/rails_ai/providers/dummy_adapter.rb +29 -0
- data/lib/rails_ai/providers/gemini_adapter.rb +509 -0
- data/lib/rails_ai/providers/openai_adapter.rb +535 -0
- data/lib/rails_ai/providers/secure_anthropic_adapter.rb +206 -0
- data/lib/rails_ai/providers/secure_openai_adapter.rb +284 -0
- data/lib/rails_ai/railtie.rb +48 -0
- data/lib/rails_ai/redactor.rb +12 -0
- data/lib/rails_ai/security/api_key_manager.rb +82 -0
- data/lib/rails_ai/security/audit_logger.rb +46 -0
- data/lib/rails_ai/security/error_handler.rb +62 -0
- data/lib/rails_ai/security/input_validator.rb +176 -0
- data/lib/rails_ai/security/secure_file_handler.rb +45 -0
- data/lib/rails_ai/security/secure_http_client.rb +177 -0
- data/lib/rails_ai/security.rb +0 -0
- data/lib/rails_ai/version.rb +5 -0
- data/lib/rails_ai/window_context.rb +103 -0
- data/lib/rails_ai.rb +502 -0
- data/monitoring/ci_setup_guide.md +214 -0
- data/monitoring/enhanced_monitoring_script.rb +237 -0
- data/monitoring/google_alerts_setup.md +42 -0
- data/monitoring_log_20250921.txt +0 -0
- data/monitoring_script.rb +161 -0
- data/rails_ai.gemspec +54 -0
- data/scripts/security_scanner.rb +353 -0
- data/setup_monitoring.sh +163 -0
- data/wiki/API-Documentation.md +734 -0
- data/wiki/Architecture-Overview.md +672 -0
- data/wiki/Contributing-Guide.md +407 -0
- data/wiki/Development-Setup.md +532 -0
- data/wiki/Home.md +278 -0
- data/wiki/Installation-Guide.md +527 -0
- data/wiki/Quick-Start.md +186 -0
- data/wiki/README.md +135 -0
- data/wiki/Release-Process.md +467 -0
- metadata +385 -0
@@ -0,0 +1,238 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsAi
|
4
|
+
module Agents
|
5
|
+
class Collaboration
|
6
|
+
attr_reader :id, :task, :agents, :manager, :status, :created_at
|
7
|
+
|
8
|
+
def initialize(task:, agents:, manager:)
|
9
|
+
@id = SecureRandom.uuid
|
10
|
+
@task = task
|
11
|
+
@agents = Array(agents)
|
12
|
+
@manager = manager
|
13
|
+
@status = :pending
|
14
|
+
@created_at = Time.now
|
15
|
+
@contributions = {}
|
16
|
+
@workflow = []
|
17
|
+
@current_phase = 0
|
18
|
+
@phases = build_workflow_phases
|
19
|
+
end
|
20
|
+
|
21
|
+
def start!
|
22
|
+
@status = :in_progress
|
23
|
+
@started_at = Time.now
|
24
|
+
|
25
|
+
defined?(Rails) && Rails.logger && Rails.logger.info("Collaboration #{@id} started with #{@agents.length} agents")
|
26
|
+
|
27
|
+
# Begin the collaboration workflow
|
28
|
+
execute_workflow_phase
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_contribution(agent_name, contribution)
|
33
|
+
@contributions[agent_name] = {
|
34
|
+
content: contribution,
|
35
|
+
timestamp: Time.now,
|
36
|
+
phase: @current_phase
|
37
|
+
}
|
38
|
+
|
39
|
+
defined?(Rails) && Rails.logger && Rails.logger.info("Agent #{agent_name} contributed to collaboration #{@id}")
|
40
|
+
|
41
|
+
# Check if current phase is complete
|
42
|
+
if phase_complete?
|
43
|
+
advance_to_next_phase
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_contributions(agent_name = nil)
|
48
|
+
if agent_name
|
49
|
+
@contributions[agent_name]
|
50
|
+
else
|
51
|
+
@contributions
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_phase_contributions(phase)
|
56
|
+
@contributions.select { |_, contrib| contrib[:phase] == phase }
|
57
|
+
end
|
58
|
+
|
59
|
+
def current_phase_info
|
60
|
+
return nil if @current_phase >= @phases.length
|
61
|
+
|
62
|
+
@phases[@current_phase]
|
63
|
+
end
|
64
|
+
|
65
|
+
def is_complete?
|
66
|
+
@status == :completed
|
67
|
+
end
|
68
|
+
|
69
|
+
def is_failed?
|
70
|
+
@status == :failed
|
71
|
+
end
|
72
|
+
|
73
|
+
def duration
|
74
|
+
return nil unless @started_at
|
75
|
+
|
76
|
+
end_time = @completed_at || Time.now
|
77
|
+
end_time - @started_at
|
78
|
+
end
|
79
|
+
|
80
|
+
def summary
|
81
|
+
{
|
82
|
+
id: @id,
|
83
|
+
task: @task,
|
84
|
+
agents: @agents.map(&:name),
|
85
|
+
status: @status,
|
86
|
+
current_phase: @current_phase,
|
87
|
+
total_phases: @phases.length,
|
88
|
+
contributions_count: @contributions.length,
|
89
|
+
duration: duration,
|
90
|
+
created_at: @created_at,
|
91
|
+
started_at: @started_at,
|
92
|
+
completed_at: @completed_at
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
def complete!(result = nil)
|
97
|
+
@status = :completed
|
98
|
+
@completed_at = Time.now
|
99
|
+
@result = result
|
100
|
+
|
101
|
+
defined?(Rails) && Rails.logger && Rails.logger.info("Collaboration #{@id} completed in #{duration&.round(2)} seconds")
|
102
|
+
|
103
|
+
# Notify all participating agents
|
104
|
+
@agents.each do |agent|
|
105
|
+
@manager.send_message("collaboration_system", agent.name, {
|
106
|
+
type: :collaboration_completed,
|
107
|
+
collaboration_id: @id,
|
108
|
+
result: result
|
109
|
+
})
|
110
|
+
end
|
111
|
+
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
def fail!(error)
|
116
|
+
@status = :failed
|
117
|
+
@failed_at = Time.now
|
118
|
+
@error = error
|
119
|
+
|
120
|
+
defined?(Rails) && Rails.logger && Rails.logger.error("Collaboration #{@id} failed: #{error}")
|
121
|
+
|
122
|
+
# Notify all participating agents
|
123
|
+
@agents.each do |agent|
|
124
|
+
@manager.send_message("collaboration_system", agent.name, {
|
125
|
+
type: :collaboration_failed,
|
126
|
+
collaboration_id: @id,
|
127
|
+
error: error
|
128
|
+
})
|
129
|
+
end
|
130
|
+
|
131
|
+
self
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def build_workflow_phases
|
137
|
+
case @task[:type] || :general
|
138
|
+
when :analysis
|
139
|
+
[
|
140
|
+
{ name: "data_gathering", description: "Gather and analyze data", required_agents: @agents.length },
|
141
|
+
{ name: "pattern_recognition", description: "Identify patterns and insights", required_agents: @agents.length },
|
142
|
+
{ name: "synthesis", description: "Synthesize findings into conclusions", required_agents: 1 }
|
143
|
+
]
|
144
|
+
when :creative
|
145
|
+
[
|
146
|
+
{ name: "brainstorming", description: "Generate creative ideas", required_agents: @agents.length },
|
147
|
+
{ name: "refinement", description: "Refine and improve ideas", required_agents: @agents.length },
|
148
|
+
{ name: "finalization", description: "Finalize the creative output", required_agents: 1 }
|
149
|
+
]
|
150
|
+
when :problem_solving
|
151
|
+
[
|
152
|
+
{ name: "problem_analysis", description: "Analyze the problem thoroughly", required_agents: @agents.length },
|
153
|
+
{ name: "solution_generation", description: "Generate potential solutions", required_agents: @agents.length },
|
154
|
+
{ name: "solution_evaluation", description: "Evaluate and select best solution", required_agents: @agents.length },
|
155
|
+
{ name: "implementation_plan", description: "Create implementation plan", required_agents: 1 }
|
156
|
+
]
|
157
|
+
else
|
158
|
+
[
|
159
|
+
{ name: "discussion", description: "Discuss the task", required_agents: @agents.length },
|
160
|
+
{ name: "consensus", description: "Reach consensus on approach", required_agents: @agents.length },
|
161
|
+
{ name: "execution", description: "Execute the agreed approach", required_agents: 1 }
|
162
|
+
]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def execute_workflow_phase
|
167
|
+
return if @current_phase >= @phases.length
|
168
|
+
|
169
|
+
phase = @phases[@current_phase]
|
170
|
+
|
171
|
+
# Notify agents about the new phase
|
172
|
+
@agents.each do |agent|
|
173
|
+
@manager.send_message("collaboration_system", agent.name, {
|
174
|
+
type: :collaboration_phase,
|
175
|
+
collaboration_id: @id,
|
176
|
+
phase: phase,
|
177
|
+
phase_number: @current_phase + 1,
|
178
|
+
total_phases: @phases.length
|
179
|
+
})
|
180
|
+
end
|
181
|
+
|
182
|
+
defined?(Rails) && Rails.logger && Rails.logger.info("Collaboration #{@id} started phase #{@current_phase + 1}: #{phase[:name]}")
|
183
|
+
end
|
184
|
+
|
185
|
+
def phase_complete?
|
186
|
+
return false if @current_phase >= @phases.length
|
187
|
+
|
188
|
+
phase = @phases[@current_phase]
|
189
|
+
current_contributions = get_phase_contributions(@current_phase)
|
190
|
+
|
191
|
+
current_contributions.length >= phase[:required_agents]
|
192
|
+
end
|
193
|
+
|
194
|
+
def advance_to_next_phase
|
195
|
+
@current_phase += 1
|
196
|
+
|
197
|
+
if @current_phase >= @phases.length
|
198
|
+
# All phases complete, synthesize final result
|
199
|
+
synthesize_result
|
200
|
+
else
|
201
|
+
# Start next phase
|
202
|
+
execute_workflow_phase
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def synthesize_result
|
207
|
+
# Combine all contributions into a final result
|
208
|
+
all_contributions = @contributions.values.map { |c| c[:content] }
|
209
|
+
|
210
|
+
synthesis_prompt = build_synthesis_prompt(all_contributions)
|
211
|
+
|
212
|
+
# Use the first agent to synthesize (or could use a dedicated synthesis agent)
|
213
|
+
synthesizer = @agents.first
|
214
|
+
result = synthesizer.think(synthesis_prompt, context: {
|
215
|
+
task: @task,
|
216
|
+
contributions: all_contributions,
|
217
|
+
collaboration_id: @id
|
218
|
+
})
|
219
|
+
|
220
|
+
complete!(result)
|
221
|
+
end
|
222
|
+
|
223
|
+
def build_synthesis_prompt(contributions)
|
224
|
+
<<~PROMPT
|
225
|
+
You are synthesizing the results of a multi-agent collaboration.
|
226
|
+
|
227
|
+
Original Task: #{@task[:description]}
|
228
|
+
|
229
|
+
Agent Contributions:
|
230
|
+
#{contributions.map.with_index { |c, i| "#{i + 1}. #{c}" }.join("\n")}
|
231
|
+
|
232
|
+
Please synthesize these contributions into a coherent, comprehensive result that addresses the original task.
|
233
|
+
Consider the different perspectives and insights provided by each agent.
|
234
|
+
PROMPT
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsAi
|
4
|
+
module Agents
|
5
|
+
class Memory
|
6
|
+
IMPORTANCE_LEVELS = {
|
7
|
+
critical: 4,
|
8
|
+
high: 3,
|
9
|
+
normal: 2,
|
10
|
+
low: 1
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
def initialize(max_size: 1000)
|
14
|
+
@max_size = max_size
|
15
|
+
@memories = []
|
16
|
+
@index = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def add(key, value, importance: :normal)
|
20
|
+
memory_item = {
|
21
|
+
key: key,
|
22
|
+
value: value,
|
23
|
+
importance: importance,
|
24
|
+
importance_score: IMPORTANCE_LEVELS[importance] || 2,
|
25
|
+
created_at: Time.now,
|
26
|
+
accessed_at: Time.now,
|
27
|
+
access_count: 0
|
28
|
+
}
|
29
|
+
|
30
|
+
# Remove oldest low-importance memory if at capacity
|
31
|
+
if @memories.length >= @max_size
|
32
|
+
remove_oldest_low_importance
|
33
|
+
end
|
34
|
+
|
35
|
+
@memories << memory_item
|
36
|
+
@index[key] = memory_item
|
37
|
+
memory_item
|
38
|
+
end
|
39
|
+
|
40
|
+
def get(key)
|
41
|
+
memory_item = @index[key]
|
42
|
+
return nil unless memory_item
|
43
|
+
|
44
|
+
memory_item[:accessed_at] = Time.now
|
45
|
+
memory_item[:access_count] += 1
|
46
|
+
memory_item[:value]
|
47
|
+
end
|
48
|
+
|
49
|
+
def remove(key)
|
50
|
+
memory_item = @index.delete(key)
|
51
|
+
return nil unless memory_item
|
52
|
+
|
53
|
+
@memories.delete(memory_item)
|
54
|
+
memory_item[:value]
|
55
|
+
end
|
56
|
+
|
57
|
+
def search(query, limit: 10)
|
58
|
+
query_lower = query.downcase
|
59
|
+
|
60
|
+
@memories
|
61
|
+
.select do |memory|
|
62
|
+
memory[:value].to_s.downcase.include?(query_lower) ||
|
63
|
+
memory[:key].to_s.downcase.include?(query_lower)
|
64
|
+
end
|
65
|
+
.sort_by { |memory| -memory[:importance_score] }
|
66
|
+
.first(limit)
|
67
|
+
.map { |memory| memory[:value] }
|
68
|
+
end
|
69
|
+
|
70
|
+
def recent(count = 10)
|
71
|
+
@memories
|
72
|
+
.sort_by { |memory| -memory[:created_at].to_f }
|
73
|
+
.first(count)
|
74
|
+
.map { |memory| { key: memory[:key], value: memory[:value], created_at: memory[:created_at] } }
|
75
|
+
end
|
76
|
+
|
77
|
+
def important(count = 5)
|
78
|
+
@memories
|
79
|
+
.select { |memory| memory[:importance_score] >= 3 }
|
80
|
+
.sort_by { |memory| -memory[:importance_score] }
|
81
|
+
.first(count)
|
82
|
+
.map { |memory| { key: memory[:key], value: memory[:value], importance: memory[:importance] } }
|
83
|
+
end
|
84
|
+
|
85
|
+
def usage_percentage
|
86
|
+
(@memories.length.to_f / @max_size * 100).round(2)
|
87
|
+
end
|
88
|
+
|
89
|
+
def clear!
|
90
|
+
@memories.clear
|
91
|
+
@index.clear
|
92
|
+
end
|
93
|
+
|
94
|
+
def size
|
95
|
+
@memories.length
|
96
|
+
end
|
97
|
+
|
98
|
+
def empty?
|
99
|
+
@memories.empty?
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def remove_oldest_low_importance
|
105
|
+
oldest_low_importance = @memories
|
106
|
+
.select { |memory| memory[:importance_score] <= 2 }
|
107
|
+
.min_by { |memory| memory[:created_at] }
|
108
|
+
|
109
|
+
if oldest_low_importance
|
110
|
+
@memories.delete(oldest_low_importance)
|
111
|
+
@index.delete(oldest_low_importance[:key])
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsAi
|
4
|
+
module Agents
|
5
|
+
class MessageBus
|
6
|
+
def initialize
|
7
|
+
@subscribers = {}
|
8
|
+
@message_history = []
|
9
|
+
@max_history = 10000
|
10
|
+
end
|
11
|
+
|
12
|
+
def subscribe(agent_name, agent)
|
13
|
+
@subscribers[agent_name] = agent
|
14
|
+
defined?(Rails) && Rails.logger && Rails.logger.info("Agent #{agent_name} subscribed to message bus")
|
15
|
+
end
|
16
|
+
|
17
|
+
def unsubscribe(agent_name)
|
18
|
+
@subscribers.delete(agent_name)
|
19
|
+
defined?(Rails) && Rails.logger && Rails.logger.info("Agent #{agent_name} unsubscribed from message bus")
|
20
|
+
end
|
21
|
+
|
22
|
+
def send_message(from_agent, to_agent, message)
|
23
|
+
return false unless @subscribers[to_agent]
|
24
|
+
|
25
|
+
message_obj = {
|
26
|
+
id: SecureRandom.uuid,
|
27
|
+
from: from_agent,
|
28
|
+
to: to_agent,
|
29
|
+
content: message,
|
30
|
+
timestamp: Time.now,
|
31
|
+
delivered: false
|
32
|
+
}
|
33
|
+
|
34
|
+
begin
|
35
|
+
@subscribers[to_agent].receive_message(message_obj)
|
36
|
+
message_obj[:delivered] = true
|
37
|
+
@message_history << message_obj
|
38
|
+
trim_history
|
39
|
+
defined?(Rails) && Rails.logger && Rails.logger.info("Message sent from #{from_agent} to #{to_agent}")
|
40
|
+
true
|
41
|
+
rescue => e
|
42
|
+
defined?(Rails) && Rails.logger && Rails.logger.error("Failed to deliver message: #{e.message}")
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def broadcast(from_agent, message, exclude: [])
|
48
|
+
delivered_count = 0
|
49
|
+
|
50
|
+
@subscribers.each do |agent_name, agent|
|
51
|
+
next if exclude.include?(agent_name) || agent_name == from_agent
|
52
|
+
|
53
|
+
if send_message(from_agent, agent_name, message)
|
54
|
+
delivered_count += 1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
defined?(Rails) && Rails.logger && Rails.logger.info("Broadcast message from #{from_agent} to #{delivered_count} agents")
|
59
|
+
delivered_count
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_messages_for_agent(agent_name, from_agent: nil, limit: 100)
|
63
|
+
messages = @message_history.select { |m| m[:to] == agent_name }
|
64
|
+
messages = messages.select { |m| m[:from] == from_agent } if from_agent
|
65
|
+
messages.last(limit)
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_message_history(limit: 1000)
|
69
|
+
@message_history.last(limit)
|
70
|
+
end
|
71
|
+
|
72
|
+
def clear_history!
|
73
|
+
@message_history.clear
|
74
|
+
defined?(Rails) && Rails.logger && Rails.logger.info("Message history cleared")
|
75
|
+
end
|
76
|
+
|
77
|
+
def stats
|
78
|
+
{
|
79
|
+
total_subscribers: @subscribers.length,
|
80
|
+
total_messages: @message_history.length,
|
81
|
+
delivered_messages: @message_history.count { |m| m[:delivered] },
|
82
|
+
failed_messages: @message_history.count { |m| !m[:delivered] }
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def trim_history
|
89
|
+
return if @message_history.length <= @max_history
|
90
|
+
|
91
|
+
@message_history = @message_history.last(@max_history)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|