layered-assistant-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 (105) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +94 -0
  3. data/LICENSE +201 -0
  4. data/README.md +176 -0
  5. data/Rakefile +15 -0
  6. data/app/assets/tailwind/layered/assistant/styles.css +13 -0
  7. data/app/controllers/concerns/layered/assistant/message_creation.rb +49 -0
  8. data/app/controllers/concerns/layered/assistant/public/session_conversations.rb +50 -0
  9. data/app/controllers/layered/assistant/application_controller.rb +25 -0
  10. data/app/controllers/layered/assistant/assistants_controller.rb +59 -0
  11. data/app/controllers/layered/assistant/conversations_controller.rb +77 -0
  12. data/app/controllers/layered/assistant/messages_controller.rb +57 -0
  13. data/app/controllers/layered/assistant/models_controller.rb +61 -0
  14. data/app/controllers/layered/assistant/panel/conversations_controller.rb +63 -0
  15. data/app/controllers/layered/assistant/panel/messages_controller.rb +44 -0
  16. data/app/controllers/layered/assistant/providers_controller.rb +55 -0
  17. data/app/controllers/layered/assistant/public/application_controller.rb +16 -0
  18. data/app/controllers/layered/assistant/public/assistants_controller.rb +16 -0
  19. data/app/controllers/layered/assistant/public/conversations_controller.rb +33 -0
  20. data/app/controllers/layered/assistant/public/messages_controller.rb +42 -0
  21. data/app/controllers/layered/assistant/public/panel/conversations_controller.rb +62 -0
  22. data/app/controllers/layered/assistant/public/panel/messages_controller.rb +50 -0
  23. data/app/controllers/layered/assistant/setup_controller.rb +9 -0
  24. data/app/helpers/layered/assistant/access_helper.rb +45 -0
  25. data/app/helpers/layered/assistant/messages_helper.rb +41 -0
  26. data/app/helpers/layered/assistant/panel_helper.rb +38 -0
  27. data/app/javascript/layered_assistant/composer_controller.js +30 -0
  28. data/app/javascript/layered_assistant/index.js +14 -0
  29. data/app/javascript/layered_assistant/message_streaming.js +124 -0
  30. data/app/javascript/layered_assistant/messages_controller.js +62 -0
  31. data/app/javascript/layered_assistant/panel_controller.js +36 -0
  32. data/app/javascript/layered_assistant/panel_nav_controller.js +16 -0
  33. data/app/javascript/layered_assistant/provider_template_controller.js +45 -0
  34. data/app/javascript/layered_assistant/vendor/marked.esm.js +72 -0
  35. data/app/jobs/layered/assistant/application_job.rb +6 -0
  36. data/app/jobs/layered/assistant/messages/response_job.rb +36 -0
  37. data/app/models/layered/assistant/application_record.rb +7 -0
  38. data/app/models/layered/assistant/assistant.rb +22 -0
  39. data/app/models/layered/assistant/conversation.rb +39 -0
  40. data/app/models/layered/assistant/message.rb +56 -0
  41. data/app/models/layered/assistant/model.rb +21 -0
  42. data/app/models/layered/assistant/provider.rb +49 -0
  43. data/app/services/layered/assistant/chunk_service.rb +80 -0
  44. data/app/services/layered/assistant/client_service.rb +18 -0
  45. data/app/services/layered/assistant/clients/anthropic.rb +24 -0
  46. data/app/services/layered/assistant/clients/base.rb +29 -0
  47. data/app/services/layered/assistant/clients/openai.rb +33 -0
  48. data/app/services/layered/assistant/messages_service.rb +58 -0
  49. data/app/services/layered/assistant/models/create_service.rb +50 -0
  50. data/app/services/layered/assistant/token_estimator.rb +11 -0
  51. data/app/views/layered/assistant/assistants/_form.html.erb +42 -0
  52. data/app/views/layered/assistant/assistants/edit.html.erb +6 -0
  53. data/app/views/layered/assistant/assistants/index.html.erb +45 -0
  54. data/app/views/layered/assistant/assistants/new.html.erb +6 -0
  55. data/app/views/layered/assistant/conversations/_form.html.erb +29 -0
  56. data/app/views/layered/assistant/conversations/edit.html.erb +6 -0
  57. data/app/views/layered/assistant/conversations/index.html.erb +63 -0
  58. data/app/views/layered/assistant/conversations/new.html.erb +6 -0
  59. data/app/views/layered/assistant/conversations/show.html.erb +25 -0
  60. data/app/views/layered/assistant/messages/_composer.html.erb +15 -0
  61. data/app/views/layered/assistant/messages/_message.html.erb +25 -0
  62. data/app/views/layered/assistant/messages/_system_prompt.html.erb +10 -0
  63. data/app/views/layered/assistant/messages/create.turbo_stream.erb +9 -0
  64. data/app/views/layered/assistant/messages/index.html.erb +46 -0
  65. data/app/views/layered/assistant/models/_form.html.erb +30 -0
  66. data/app/views/layered/assistant/models/edit.html.erb +6 -0
  67. data/app/views/layered/assistant/models/index.html.erb +54 -0
  68. data/app/views/layered/assistant/models/new.html.erb +6 -0
  69. data/app/views/layered/assistant/panel/conversations/_header.html.erb +23 -0
  70. data/app/views/layered/assistant/panel/conversations/index.html.erb +46 -0
  71. data/app/views/layered/assistant/panel/conversations/new.html.erb +23 -0
  72. data/app/views/layered/assistant/panel/conversations/show.html.erb +24 -0
  73. data/app/views/layered/assistant/panel/messages/_composer.html.erb +15 -0
  74. data/app/views/layered/assistant/panel/messages/create.turbo_stream.erb +9 -0
  75. data/app/views/layered/assistant/providers/_form.html.erb +81 -0
  76. data/app/views/layered/assistant/providers/edit.html.erb +6 -0
  77. data/app/views/layered/assistant/providers/index.html.erb +47 -0
  78. data/app/views/layered/assistant/providers/new.html.erb +6 -0
  79. data/app/views/layered/assistant/public/assistants/index.html.erb +34 -0
  80. data/app/views/layered/assistant/public/assistants/show.html.erb +23 -0
  81. data/app/views/layered/assistant/public/conversations/show.html.erb +24 -0
  82. data/app/views/layered/assistant/public/messages/_composer.html.erb +7 -0
  83. data/app/views/layered/assistant/public/messages/create.turbo_stream.erb +9 -0
  84. data/app/views/layered/assistant/public/panel/conversations/_header.html.erb +16 -0
  85. data/app/views/layered/assistant/public/panel/conversations/index.html.erb +48 -0
  86. data/app/views/layered/assistant/public/panel/conversations/new.html.erb +17 -0
  87. data/app/views/layered/assistant/public/panel/conversations/show.html.erb +23 -0
  88. data/app/views/layered/assistant/public/panel/messages/_composer.html.erb +7 -0
  89. data/app/views/layered/assistant/public/panel/messages/create.turbo_stream.erb +9 -0
  90. data/app/views/layered/assistant/setup/_setup.html.erb +121 -0
  91. data/app/views/layered/assistant/setup/index.html.erb +2 -0
  92. data/app/views/layouts/layered/assistant/_host_navigation.html.erb +0 -0
  93. data/app/views/layouts/layered/assistant/application.html.erb +32 -0
  94. data/config/importmap.rb +8 -0
  95. data/config/routes.rb +31 -0
  96. data/data/models.json +42 -0
  97. data/db/migrate/20260312000000_create_layered_assistant_tables.rb +63 -0
  98. data/lib/generators/layered/assistant/install_generator.rb +113 -0
  99. data/lib/generators/layered/assistant/migrations_generator.rb +47 -0
  100. data/lib/generators/layered/assistant/templates/initializer.rb +26 -0
  101. data/lib/layered/assistant/engine.rb +29 -0
  102. data/lib/layered/assistant/version.rb +5 -0
  103. data/lib/layered/assistant.rb +19 -0
  104. data/lib/layered-assistant-rails.rb +1 -0
  105. metadata +449 -0
@@ -0,0 +1,113 @@
1
+ module Layered
2
+ module Assistant
3
+ module Generators
4
+ class InstallGenerator < Rails::Generators::Base
5
+ desc "Install layered-ui-assistant into the host application"
6
+
7
+ def self.source_root
8
+ Layered::Assistant::Engine.root
9
+ end
10
+
11
+ def verify_layered_ui_dependency
12
+ application_css = "app/assets/tailwind/application.css"
13
+ application_js = "app/javascript/application.js"
14
+
15
+ css_ok = File.exist?(application_css) && File.read(application_css).include?('@import "./layered_ui"')
16
+ js_ok = File.exist?(application_js) && File.read(application_js).include?('import "layered_ui"')
17
+
18
+ unless css_ok && js_ok
19
+ say "layered-ui-rails is not installed yet - installing now...", :yellow
20
+ invoke "layered:ui:install"
21
+ end
22
+ end
23
+
24
+ def copy_css
25
+ source_path = File.join(self.class.source_root, "app/assets/tailwind/layered/assistant/styles.css")
26
+ source_content = File.read(source_path)
27
+
28
+ header = <<~CSS
29
+ /*
30
+ * layered-assistant-rails v#{Layered::Assistant::VERSION}
31
+ *
32
+ * This file was automatically generated by the layered:assistant:install generator.
33
+ * Do not modify directly. To update, re-run: bin/rails generate layered:assistant:install
34
+ */
35
+
36
+ CSS
37
+
38
+ create_file "app/assets/tailwind/layered_assistant.css", header + source_content, force: true
39
+ end
40
+
41
+ def add_css_import
42
+ application_css = "app/assets/tailwind/application.css"
43
+
44
+ return unless File.exist?(application_css)
45
+
46
+ content = File.read(application_css)
47
+ import_line = '@import "./layered_assistant";'
48
+
49
+ return if content.include?(import_line)
50
+
51
+ # Insert after the layered_ui import (which must already be present)
52
+ inject_into_file application_css, "\n#{import_line}", after: '@import "./layered_ui";'
53
+ say "Added import to #{application_css}", :green
54
+ end
55
+
56
+ def add_js_import
57
+ application_js = "app/javascript/application.js"
58
+
59
+ return unless File.exist?(application_js)
60
+
61
+ content = File.read(application_js)
62
+ import_line = 'import "layered_assistant"'
63
+
64
+ return if content.include?(import_line)
65
+
66
+ # Insert after the layered_ui import (which must already be present)
67
+ inject_into_file application_js, "\n#{import_line}", after: 'import "layered_ui"'
68
+ say "Added import to #{application_js}", :green
69
+ end
70
+
71
+ def copy_initializer
72
+ initializer_path = "config/initializers/layered_assistant.rb"
73
+
74
+ return if File.exist?(initializer_path)
75
+
76
+ template_path = File.expand_path("templates/initializer.rb", __dir__)
77
+ create_file initializer_path, File.read(template_path)
78
+ say "Created #{initializer_path}", :green
79
+ end
80
+
81
+ def mount_engine
82
+ routes_file = "config/routes.rb"
83
+
84
+ return unless File.exist?(routes_file)
85
+
86
+ content = File.read(routes_file)
87
+ mount_line = 'mount Layered::Assistant::Engine => "/layered/assistant"'
88
+
89
+ return if content.include?("Layered::Assistant::Engine")
90
+
91
+ inject_into_file routes_file, " #{mount_line}\n\n", after: "Rails.application.routes.draw do\n"
92
+ say "Mounted engine in #{routes_file}", :green
93
+ end
94
+
95
+ def copy_migrations
96
+ invoke "layered:assistant:migrations"
97
+ end
98
+
99
+ def show_instructions
100
+ say ""
101
+ say "layered-assistant-rails installed successfully", :green
102
+ say ""
103
+ say "Next steps:", :yellow
104
+ say " 1. Run bin/rails db:migrate"
105
+ say " 2. Configure authorization in config/initializers/layered_assistant.rb"
106
+ say " 3. Routes return 403 until you uncomment an authorize block"
107
+ say " 4. Visit /layered/assistant to get started"
108
+ say ""
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,47 @@
1
+ module Layered
2
+ module Assistant
3
+ module Generators
4
+ class MigrationsGenerator < Rails::Generators::Base
5
+ desc "Copy layered-assistant-rails migrations to the host application"
6
+
7
+ def copy_migrations
8
+ engine_migrations_path = Layered::Assistant::Engine.root.join("db/migrate")
9
+ app_migrations_path = Rails.root.join("db/migrate")
10
+
11
+ unless engine_migrations_path.exist?
12
+ say "No migrations found in layered-assistant-rails.", :yellow
13
+ return
14
+ end
15
+
16
+ existing_migrations = Dir[app_migrations_path.join("*.rb")].map { |f| migration_name(File.basename(f)) }
17
+
18
+ timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
19
+
20
+ Dir[engine_migrations_path.join("*.rb")].sort.each do |source|
21
+ basename = File.basename(source)
22
+ name = migration_name(basename)
23
+
24
+ if existing_migrations.include?(name)
25
+ say " skip #{name} (already exists)", :yellow
26
+ next
27
+ end
28
+
29
+ destination = app_migrations_path.join("#{timestamp}_#{name}.layered_assistant.rb")
30
+ content = "# This migration comes from layered_assistant (originally #{basename.split("_").first})\n" + File.read(source)
31
+
32
+ create_file destination, content
33
+ timestamp += 1
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ # Extract migration name without timestamp prefix
40
+ # e.g. "20260202074215_create_layered_assistant_providers.rb" => "create_layered_assistant_providers"
41
+ def migration_name(filename)
42
+ filename.sub(/^\d+_/, "").sub(/\.layered_assistant\.rb$/, "").sub(/\.rb$/, "")
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,26 @@
1
+ # Configure authorization for layered-assistant-rails.
2
+ # All non-public engine routes return 403 Forbidden until this block is configured.
3
+ # Uncomment one of the examples below, or write your own.
4
+ #
5
+ # The block runs in controller context, so you have access to request,
6
+ # current_user, redirect_to, head, main_app, and all other controller methods.
7
+ #
8
+ # Once configured, visit /layered/assistant (or your mount path) to get started.
9
+
10
+ # Allow all requests (no authorization):
11
+ #
12
+ # Layered::Assistant.authorize do
13
+ # # No-op: all requests permitted
14
+ # end
15
+
16
+ # Require sign-in (Devise):
17
+ #
18
+ # Layered::Assistant.authorize do
19
+ # redirect_to main_app.new_user_session_path unless user_signed_in?
20
+ # end
21
+
22
+ # Restrict to admins:
23
+ #
24
+ # Layered::Assistant.authorize do
25
+ # head :forbidden unless current_user&.admin?
26
+ # end
@@ -0,0 +1,29 @@
1
+ module Layered
2
+ module Assistant
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace Layered::Assistant
5
+
6
+ initializer "layered-assistant-rails.inflections", before: :set_autoload_paths do
7
+ ActiveSupport::Inflector.inflections do |inflect|
8
+ inflect.acronym "OpenAI"
9
+ end
10
+ end
11
+
12
+ initializer "layered-assistant-rails.importmap", before: "importmap" do |app|
13
+ app.config.importmap.paths << Engine.root.join("config/importmap.rb")
14
+ end
15
+
16
+ initializer "layered-assistant-rails.assets" do |app|
17
+ app.config.assets.paths << Engine.root.join("app/javascript")
18
+ end
19
+
20
+ initializer "layered-assistant-rails.helpers" do
21
+ ActiveSupport.on_load(:action_controller_base) do
22
+ helper Layered::Assistant::AccessHelper
23
+ helper Layered::Assistant::MessagesHelper
24
+ helper Layered::Assistant::PanelHelper
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ module Layered
2
+ module Assistant
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ require "positioning"
2
+ require "pagy"
3
+ require "anthropic"
4
+ require "openai"
5
+ require "kramdown"
6
+ require "kramdown-parser-gfm"
7
+ require "layered-ui-rails"
8
+ require "layered/assistant/version"
9
+ require "layered/assistant/engine"
10
+
11
+ module Layered
12
+ module Assistant
13
+ mattr_reader :authorize_block
14
+
15
+ def self.authorize(&block)
16
+ @@authorize_block = block
17
+ end
18
+ end
19
+ end
@@ -0,0 +1 @@
1
+ require "layered/assistant"