deepagents_rails 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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +309 -0
  4. data/lib/deepagents_rails/service.rb +93 -0
  5. data/lib/deepagents_rails/version.rb +3 -0
  6. data/lib/deepagents_rails.rb +31 -0
  7. data/lib/generators/deepagents/agent/agent_generator.rb +55 -0
  8. data/lib/generators/deepagents/agent/templates/agent.rb +99 -0
  9. data/lib/generators/deepagents/agent/templates/agent_spec.rb +33 -0
  10. data/lib/generators/deepagents/agent/templates/controller.rb +44 -0
  11. data/lib/generators/deepagents/agent/templates/index.html.erb +178 -0
  12. data/lib/generators/deepagents/agent/templates/show.html.erb +131 -0
  13. data/lib/generators/deepagents/controller/controller_generator.rb +51 -0
  14. data/lib/generators/deepagents/controller/templates/api_controller.rb +87 -0
  15. data/lib/generators/deepagents/controller/templates/serializer.rb +22 -0
  16. data/lib/generators/deepagents/install/install_generator.rb +41 -0
  17. data/lib/generators/deepagents/install/templates/deepagents.yml +38 -0
  18. data/lib/generators/deepagents/install/templates/initializer.rb +35 -0
  19. data/lib/generators/deepagents/model/model_generator.rb +43 -0
  20. data/lib/generators/deepagents/model/templates/conversation.rb +73 -0
  21. data/lib/generators/deepagents/model/templates/create_conversations_migration.rb +16 -0
  22. data/lib/generators/deepagents/model/templates/create_files_migration.rb +17 -0
  23. data/lib/generators/deepagents/model/templates/create_messages_migration.rb +17 -0
  24. data/lib/generators/deepagents/model/templates/file.rb +96 -0
  25. data/lib/generators/deepagents/model/templates/message.rb +30 -0
  26. data/lib/generators/deepagents/tool/templates/tool.rb +27 -0
  27. data/lib/generators/deepagents/tool/templates/tool_spec.rb +36 -0
  28. data/lib/generators/deepagents/tool/tool_generator.rb +58 -0
  29. data/lib/generators/deepagents/view/templates/_conversation.html.erb +10 -0
  30. data/lib/generators/deepagents/view/templates/_form.html.erb +33 -0
  31. data/lib/generators/deepagents/view/templates/_message.html.erb +31 -0
  32. data/lib/generators/deepagents/view/templates/index.html.erb +35 -0
  33. data/lib/generators/deepagents/view/templates/javascript.js +199 -0
  34. data/lib/generators/deepagents/view/templates/new.html.erb +10 -0
  35. data/lib/generators/deepagents/view/templates/show.html.erb +54 -0
  36. data/lib/generators/deepagents/view/templates/stylesheet.css +397 -0
  37. data/lib/generators/deepagents/view/view_generator.rb +50 -0
  38. metadata +121 -0
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deepagents
4
+ class <%= class_name %>Conversation < ApplicationRecord
5
+ self.table_name = 'deepagents_<%= file_name %>_conversations'
6
+
7
+ has_many :<%= file_name %>_messages, class_name: 'Deepagents::<%= class_name %>Message', foreign_key: 'conversation_id', dependent: :destroy
8
+ has_many :<%= file_name %>_files, class_name: 'Deepagents::<%= class_name %>File', foreign_key: 'conversation_id', dependent: :destroy
9
+
10
+ # Alias for easier access
11
+ alias_method :messages, :<%= file_name %>_messages
12
+ alias_method :files, :<%= file_name %>_files
13
+
14
+ # Scopes
15
+ scope :recent, -> { order(created_at: :desc) }
16
+
17
+ # Convert to format expected by DeepAgents
18
+ def to_agent_format
19
+ {
20
+ messages: messages.order(:created_at).map(&:to_agent_format),
21
+ files: files_hash
22
+ }
23
+ end
24
+
25
+ # Run the agent with the current conversation context
26
+ def run_agent(input, agent_name = nil)
27
+ # Create a new message for the user input
28
+ user_message = messages.create!(
29
+ role: 'user',
30
+ content: input
31
+ )
32
+
33
+ # Get the service
34
+ service = DeepagentsRails.service(agent_name)
35
+
36
+ # Run the agent with the conversation context
37
+ result = service.run(input, to_agent_format)
38
+
39
+ # Create a new message for the agent response
40
+ assistant_message = messages.create!(
41
+ role: 'assistant',
42
+ content: result[:response]
43
+ )
44
+
45
+ # Save any files generated by the agent
46
+ if result[:files].present?
47
+ result[:files].each do |filename, content|
48
+ files.create!(
49
+ filename: filename,
50
+ content: content
51
+ )
52
+ end
53
+ end
54
+
55
+ # Return the result
56
+ {
57
+ message: assistant_message,
58
+ files: result[:files]
59
+ }
60
+ end
61
+
62
+ private
63
+
64
+ # Convert files to hash format expected by DeepAgents
65
+ def files_hash
66
+ hash = {}
67
+ files.each do |file|
68
+ hash[file.filename] = file.content
69
+ end
70
+ hash
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDeepagents<%= class_name.pluralize %>Conversations < ActiveRecord::Migration[6.0]
4
+ def change
5
+ create_table :deepagents_<%= file_name %>_conversations do |t|
6
+ t.string :title
7
+ t.references :user, polymorphic: true, index: { name: 'index_<%= file_name %>_conversations_on_user' }
8
+ t.string :agent_name
9
+ t.jsonb :metadata, default: {}
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :deepagents_<%= file_name %>_conversations, :created_at
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDeepagents<%= class_name.pluralize %>Files < ActiveRecord::Migration[6.0]
4
+ def change
5
+ create_table :deepagents_<%= file_name %>_files do |t|
6
+ t.references :conversation, null: false, foreign_key: { to_table: :deepagents_<%= file_name %>_conversations }, index: { name: 'index_<%= file_name %>_files_on_conversation_id' }
7
+ t.string :filename, null: false
8
+ t.text :content, null: false
9
+ t.jsonb :metadata, default: {}
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :deepagents_<%= file_name %>_files, :created_at
15
+ add_index :deepagents_<%= file_name %>_files, :filename
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDeepagents<%= class_name.pluralize %>Messages < ActiveRecord::Migration[6.0]
4
+ def change
5
+ create_table :deepagents_<%= file_name %>_messages do |t|
6
+ t.references :conversation, null: false, foreign_key: { to_table: :deepagents_<%= file_name %>_conversations }, index: { name: 'index_<%= file_name %>_messages_on_conversation_id' }
7
+ t.string :role, null: false
8
+ t.text :content, null: false
9
+ t.jsonb :metadata, default: {}
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :deepagents_<%= file_name %>_messages, :created_at
15
+ add_index :deepagents_<%= file_name %>_messages, :role
16
+ end
17
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deepagents
4
+ class <%= class_name %>File < ApplicationRecord
5
+ self.table_name = 'deepagents_<%= file_name %>_files'
6
+
7
+ belongs_to :<%= file_name %>_conversation, class_name: 'Deepagents::<%= class_name %>Conversation', foreign_key: 'conversation_id'
8
+
9
+ # Alias for easier access
10
+ alias_method :conversation, :<%= file_name %>_conversation
11
+
12
+ # Validations
13
+ validates :filename, presence: true
14
+ validates :content, presence: true
15
+
16
+ # Get the file extension
17
+ def extension
18
+ File.extname(filename).delete('.')
19
+ end
20
+
21
+ # Check if the file is an image
22
+ def image?
23
+ %w[jpg jpeg png gif svg webp].include?(extension.downcase)
24
+ end
25
+
26
+ # Check if the file is a text file
27
+ def text?
28
+ %w[txt md markdown rb py js html css json yaml yml xml].include?(extension.downcase)
29
+ end
30
+
31
+ # Generate a URL for the file (if using Active Storage)
32
+ def url
33
+ if defined?(ActiveStorage) && attachment.present?
34
+ Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
35
+ else
36
+ "#"
37
+ end
38
+ end
39
+
40
+ # Attach the file content to Active Storage (if available)
41
+ def attach_content
42
+ return unless defined?(ActiveStorage)
43
+
44
+ # Create a tempfile with the content
45
+ tempfile = Tempfile.new([filename, ".#{extension}"])
46
+ tempfile.binmode
47
+ tempfile.write(content)
48
+ tempfile.rewind
49
+
50
+ # Attach the tempfile
51
+ attachment.attach(
52
+ io: tempfile,
53
+ filename: filename,
54
+ content_type: content_type
55
+ )
56
+
57
+ # Close and delete the tempfile
58
+ tempfile.close
59
+ tempfile.unlink
60
+ end
61
+
62
+ private
63
+
64
+ # Determine the content type based on the extension
65
+ def content_type
66
+ case extension.downcase
67
+ when 'jpg', 'jpeg'
68
+ 'image/jpeg'
69
+ when 'png'
70
+ 'image/png'
71
+ when 'gif'
72
+ 'image/gif'
73
+ when 'svg'
74
+ 'image/svg+xml'
75
+ when 'txt'
76
+ 'text/plain'
77
+ when 'md', 'markdown'
78
+ 'text/markdown'
79
+ when 'html'
80
+ 'text/html'
81
+ when 'css'
82
+ 'text/css'
83
+ when 'js'
84
+ 'application/javascript'
85
+ when 'json'
86
+ 'application/json'
87
+ when 'yaml', 'yml'
88
+ 'application/yaml'
89
+ when 'xml'
90
+ 'application/xml'
91
+ else
92
+ 'application/octet-stream'
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deepagents
4
+ class <%= class_name %>Message < ApplicationRecord
5
+ self.table_name = 'deepagents_<%= file_name %>_messages'
6
+
7
+ belongs_to :<%= file_name %>_conversation, class_name: 'Deepagents::<%= class_name %>Conversation', foreign_key: 'conversation_id'
8
+
9
+ # Alias for easier access
10
+ alias_method :conversation, :<%= file_name %>_conversation
11
+
12
+ # Validations
13
+ validates :role, presence: true, inclusion: { in: %w[user assistant system] }
14
+ validates :content, presence: true
15
+
16
+ # Scopes
17
+ scope :user, -> { where(role: 'user') }
18
+ scope :assistant, -> { where(role: 'assistant') }
19
+ scope :system, -> { where(role: 'system') }
20
+ scope :chronological, -> { order(created_at: :asc) }
21
+
22
+ # Convert to format expected by DeepAgents
23
+ def to_agent_format
24
+ {
25
+ role: role,
26
+ content: content
27
+ }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deepagents
4
+ module Tools
5
+ class <%= class_name %>Tool
6
+ # Returns a DeepAgents::Tool instance
7
+ def self.build
8
+ DeepAgents::Tool.new(
9
+ "<%= file_name %>",
10
+ "<%= tool_description %>"
11
+ ) do |<%= parameters_with_defaults %>|
12
+ # Implement your tool's functionality here
13
+ # This block will be called when the agent uses this tool
14
+
15
+ begin
16
+ # Example implementation - replace with your actual logic
17
+ "Result of <%= file_name %> operation with parameters: #{[<%= parameters_as_args %>].join(', ')}"
18
+ rescue => e
19
+ "Error in <%= file_name %> tool: #{e.message}"
20
+ end
21
+ end
22
+ end
23
+
24
+ # Optional: Add helper methods for your tool below
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe Deepagents::Tools::<%= class_name %>Tool do
6
+ describe '.build' do
7
+ it 'returns a DeepAgents::Tool instance' do
8
+ tool = described_class.build
9
+ expect(tool).to be_a(DeepAgents::Tool)
10
+ expect(tool.name).to eq('<%= file_name %>')
11
+ end
12
+
13
+ it 'has the correct description' do
14
+ tool = described_class.build
15
+ expect(tool.description).to eq('<%= tool_description %>')
16
+ end
17
+
18
+ it 'executes the tool functionality' do
19
+ tool = described_class.build
20
+ # Test with sample parameters - adjust based on your tool's parameters
21
+ result = tool.execute(<% tool_parameters.each_with_index do |param, i| %><%= i > 0 ? ', ' : '' %>'test_<%= param %>'<% end %>)
22
+ expect(result).to be_a(String)
23
+ expect(result).to include('Result of <%= file_name %> operation')
24
+ end
25
+
26
+ it 'handles errors gracefully' do
27
+ tool = described_class.build
28
+
29
+ # Mock an error in the tool execution
30
+ allow_any_instance_of(DeepAgents::Tool).to receive(:execute).and_raise(StandardError.new('Test error'))
31
+
32
+ # The tool should catch the error and return an error message
33
+ expect { tool.execute(<% tool_parameters.each_with_index do |param, i| %><%= i > 0 ? ', ' : '' %>'test_<%= param %>'<% end %>) }.not_to raise_error
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,58 @@
1
+ module Deepagents
2
+ module Generators
3
+ class ToolGenerator < Rails::Generators::NamedBase
4
+ source_root File.expand_path('templates', __dir__)
5
+
6
+ desc "Creates a new DeepAgents tool for your Rails application"
7
+
8
+ class_option :description, type: :string, default: "", desc: "Description of what the tool does"
9
+ class_option :parameters, type: :array, default: [], desc: "Parameters for the tool"
10
+
11
+ def create_tool_file
12
+ template "tool.rb", "app/deepagents/tools/#{file_name}_tool.rb"
13
+ end
14
+
15
+ def create_tool_spec_file
16
+ template "tool_spec.rb", "spec/deepagents/tools/#{file_name}_tool_spec.rb"
17
+ end
18
+
19
+ def display_next_steps
20
+ say "\n"
21
+ say "Tool #{file_name} has been created! 🔧", :green
22
+ say "\n"
23
+ say "Next steps:", :yellow
24
+ say " 1. Edit app/deepagents/tools/#{file_name}_tool.rb to implement your tool's functionality"
25
+ say " 2. Add this tool to your agents by including it in the tools array"
26
+ say "\n"
27
+ end
28
+
29
+ private
30
+
31
+ def tool_description
32
+ options[:description].present? ? options[:description] : "Performs #{file_name} operations"
33
+ end
34
+
35
+ def tool_parameters
36
+ options[:parameters].empty? ? ["query"] : options[:parameters]
37
+ end
38
+
39
+ def parameters_as_args
40
+ params = tool_parameters
41
+ if params.size == 1
42
+ params.first
43
+ else
44
+ params.join(", ")
45
+ end
46
+ end
47
+
48
+ def parameters_with_defaults
49
+ params = tool_parameters
50
+ if params.size == 1
51
+ "#{params.first}"
52
+ else
53
+ params.map { |p| "#{p}" }.join(", ")
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,10 @@
1
+ <%# DeepAgents %> <%= class_name %> <%# Conversation Partial %>
2
+ <% if conversation.messages.any? %>
3
+ <% conversation.messages.order(:created_at).each do |message| %>
4
+ <%= render partial: "message", locals: { message: message } %>
5
+ <% end %>
6
+ <% else %>
7
+ <div class="deepagents-empty-state">
8
+ <p>No messages yet. Start the conversation by typing a message below.</p>
9
+ </div>
10
+ <% end %>
@@ -0,0 +1,33 @@
1
+ <%# DeepAgents %> <%= class_name %> <%# Form Partial %>
2
+ <%= form_with(model: #{file_name}, local: true, class: "deepagents-form") do |form| %>
3
+ <% if #{file_name}.errors.any? %>
4
+ <div class="deepagents-error-messages">
5
+ <h2><%= pluralize(#{file_name}.errors.count, "error") %> prohibited this conversation from being saved:</h2>
6
+ <ul>
7
+ <% #{file_name}.errors.full_messages.each do |message| %>
8
+ <li><%= message %></li>
9
+ <% end %>
10
+ </ul>
11
+ </div>
12
+ <% end %>
13
+
14
+ <div class="deepagents-form-group">
15
+ <%= form.label :title %>
16
+ <%= form.text_field :title, class: "deepagents-form-control" %>
17
+ </div>
18
+
19
+ <div class="deepagents-form-group">
20
+ <%= form.label :agent_name %>
21
+ <%= form.select :agent_name, DeepagentsRails.service.available_agents.map { |name| [name.titleize, name] }, { include_blank: "Default Agent" }, class: "deepagents-form-control" %>
22
+ </div>
23
+
24
+ <div class="deepagents-form-group">
25
+ <%= form.label :system_message, "System Instructions" %>
26
+ <%= form.text_area :system_message, class: "deepagents-form-control", rows: 3 %>
27
+ <small class="deepagents-form-text">Optional instructions to guide the agent's behavior</small>
28
+ </div>
29
+
30
+ <div class="deepagents-form-actions">
31
+ <%= form.submit "Create Conversation", class: "deepagents-button" %>
32
+ </div>
33
+ <% end %>
@@ -0,0 +1,31 @@
1
+ <%# DeepAgents %> <%= class_name %> <%# Message Partial %>
2
+ <div class="deepagents-message <%= message.role %>">
3
+ <div class="deepagents-message-avatar">
4
+ <% if message.role == 'user' %>
5
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
6
+ <path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10z"/>
7
+ </svg>
8
+ <% elsif message.role == 'assistant' %>
9
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
10
+ <path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5ZM3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.58 26.58 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.933.933 0 0 1-.765.935c-.845.147-2.34.346-4.235.346-1.895 0-3.39-.2-4.235-.346A.933.933 0 0 1 3 9.219V8.062Zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a24.767 24.767 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25.286 25.286 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135Z"/>
11
+ <path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2V1.866ZM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5Z"/>
12
+ </svg>
13
+ <% else %>
14
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
15
+ <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
16
+ <path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
17
+ </svg>
18
+ <% end %>
19
+ </div>
20
+
21
+ <div class="deepagents-message-content">
22
+ <div class="deepagents-message-header">
23
+ <span class="deepagents-message-role"><%= message.role.capitalize %></span>
24
+ <span class="deepagents-message-timestamp"><%= time_ago_in_words(message.created_at) %> ago</span>
25
+ </div>
26
+
27
+ <div class="deepagents-message-body">
28
+ <%= simple_format(message.content) %>
29
+ </div>
30
+ </div>
31
+ </div>
@@ -0,0 +1,35 @@
1
+ <%# DeepAgents %> <%= class_name %> <%# Index View %>
2
+ <div class="deepagents-container">
3
+ <h1>Conversations</h1>
4
+
5
+ <div class="deepagents-actions">
6
+ <%= link_to "New Conversation", new_#{file_name}_path, class: "deepagents-button" %>
7
+ </div>
8
+
9
+ <div class="deepagents-conversations-list">
10
+ <% if @conversations.any? %>
11
+ <% @conversations.each do |conversation| %>
12
+ <div class="deepagents-conversation-card">
13
+ <h3><%= link_to conversation.title.presence || "Conversation ##{conversation.id}", #{file_name}_path(conversation) %></h3>
14
+ <div class="deepagents-conversation-meta">
15
+ <span class="deepagents-timestamp"><%= time_ago_in_words(conversation.created_at) %> ago</span>
16
+ <% if conversation.agent_name.present? %>
17
+ <span class="deepagents-agent-name"><%= conversation.agent_name %></span>
18
+ <% end %>
19
+ </div>
20
+ <div class="deepagents-conversation-preview">
21
+ <% if conversation.messages.any? %>
22
+ <%= truncate(conversation.messages.last.content, length: 100) %>
23
+ <% else %>
24
+ <em>No messages yet</em>
25
+ <% end %>
26
+ </div>
27
+ </div>
28
+ <% end %>
29
+ <% else %>
30
+ <div class="deepagents-empty-state">
31
+ <p>No conversations yet. <%= link_to "Start a new conversation", new_#{file_name}_path %>.</p>
32
+ </div>
33
+ <% end %>
34
+ </div>
35
+ </div>