rubyllm-observ 0.5.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/README.md +778 -0
- data/Rakefile +49 -0
- data/app/assets/javascripts/observ/application.js +12 -0
- data/app/assets/javascripts/observ/controllers/autoscroll_controller.js +33 -0
- data/app/assets/javascripts/observ/controllers/chat_form_controller.js +93 -0
- data/app/assets/javascripts/observ/controllers/copy_controller.js +43 -0
- data/app/assets/javascripts/observ/controllers/dashboard_controller.js +58 -0
- data/app/assets/javascripts/observ/controllers/drawer_controller.js +58 -0
- data/app/assets/javascripts/observ/controllers/expandable_controller.js +33 -0
- data/app/assets/javascripts/observ/controllers/filter_controller.js +36 -0
- data/app/assets/javascripts/observ/controllers/index.js +52 -0
- data/app/assets/javascripts/observ/controllers/json_viewer_controller.js +260 -0
- data/app/assets/javascripts/observ/controllers/message_form_controller.js +58 -0
- data/app/assets/javascripts/observ/controllers/prompt_variables_controller.js +64 -0
- data/app/assets/javascripts/observ/controllers/text_select_controller.js +14 -0
- data/app/assets/stylesheets/observ/_annotations.scss +127 -0
- data/app/assets/stylesheets/observ/_card.scss +52 -0
- data/app/assets/stylesheets/observ/_chat.scss +156 -0
- data/app/assets/stylesheets/observ/_components.scss +460 -0
- data/app/assets/stylesheets/observ/_dashboard.scss +40 -0
- data/app/assets/stylesheets/observ/_datasets.scss +697 -0
- data/app/assets/stylesheets/observ/_drawer.scss +273 -0
- data/app/assets/stylesheets/observ/_json_viewer.scss +120 -0
- data/app/assets/stylesheets/observ/_layout.scss +256 -0
- data/app/assets/stylesheets/observ/_metrics.scss +99 -0
- data/app/assets/stylesheets/observ/_observations.scss +160 -0
- data/app/assets/stylesheets/observ/_pagination.scss +143 -0
- data/app/assets/stylesheets/observ/_prompts.scss +365 -0
- data/app/assets/stylesheets/observ/_table.scss +53 -0
- data/app/assets/stylesheets/observ/_variables.scss +53 -0
- data/app/assets/stylesheets/observ/application.scss +15 -0
- data/app/controllers/observ/annotations_controller.rb +144 -0
- data/app/controllers/observ/application_controller.rb +8 -0
- data/app/controllers/observ/chats_controller.rb +58 -0
- data/app/controllers/observ/dashboard_controller.rb +159 -0
- data/app/controllers/observ/dataset_items_controller.rb +85 -0
- data/app/controllers/observ/dataset_run_items_controller.rb +84 -0
- data/app/controllers/observ/dataset_runs_controller.rb +110 -0
- data/app/controllers/observ/datasets_controller.rb +74 -0
- data/app/controllers/observ/messages_controller.rb +26 -0
- data/app/controllers/observ/observations_controller.rb +59 -0
- data/app/controllers/observ/prompt_versions_controller.rb +148 -0
- data/app/controllers/observ/prompts_controller.rb +205 -0
- data/app/controllers/observ/sessions_controller.rb +45 -0
- data/app/controllers/observ/traces_controller.rb +86 -0
- data/app/forms/observ/prompt_form.rb +96 -0
- data/app/helpers/observ/application_helper.rb +9 -0
- data/app/helpers/observ/chats_helper.rb +47 -0
- data/app/helpers/observ/dashboard_helper.rb +154 -0
- data/app/helpers/observ/datasets_helper.rb +62 -0
- data/app/helpers/observ/pagination_helper.rb +38 -0
- data/app/jobs/observ/application_job.rb +4 -0
- data/app/jobs/observ/dataset_runner_job.rb +49 -0
- data/app/mailers/observ/application_mailer.rb +6 -0
- data/app/models/concerns/observ/agent_phaseable.rb +124 -0
- data/app/models/concerns/observ/agent_selectable.rb +50 -0
- data/app/models/concerns/observ/chat_enhancements.rb +109 -0
- data/app/models/concerns/observ/message_enhancements.rb +31 -0
- data/app/models/concerns/observ/observability_instrumentation.rb +124 -0
- data/app/models/concerns/observ/prompt_management.rb +320 -0
- data/app/models/concerns/observ/trace_association.rb +9 -0
- data/app/models/observ/annotation.rb +23 -0
- data/app/models/observ/application_record.rb +5 -0
- data/app/models/observ/dataset.rb +51 -0
- data/app/models/observ/dataset_item.rb +41 -0
- data/app/models/observ/dataset_run.rb +104 -0
- data/app/models/observ/dataset_run_item.rb +111 -0
- data/app/models/observ/generation.rb +56 -0
- data/app/models/observ/null_prompt.rb +59 -0
- data/app/models/observ/observation.rb +38 -0
- data/app/models/observ/prompt.rb +315 -0
- data/app/models/observ/score.rb +51 -0
- data/app/models/observ/session.rb +131 -0
- data/app/models/observ/span.rb +13 -0
- data/app/models/observ/trace.rb +135 -0
- data/app/presenters/observ/agent_select_presenter.rb +59 -0
- data/app/services/observ/agent_executor_service.rb +174 -0
- data/app/services/observ/agent_provider.rb +60 -0
- data/app/services/observ/agent_selection_service.rb +53 -0
- data/app/services/observ/chat_instrumenter.rb +523 -0
- data/app/services/observ/dataset_runner_service.rb +153 -0
- data/app/services/observ/evaluator_runner_service.rb +58 -0
- data/app/services/observ/evaluators/base_evaluator.rb +51 -0
- data/app/services/observ/evaluators/contains_evaluator.rb +53 -0
- data/app/services/observ/evaluators/exact_match_evaluator.rb +23 -0
- data/app/services/observ/evaluators/json_structure_evaluator.rb +44 -0
- data/app/services/observ/prompt_manager/cache_statistics.rb +82 -0
- data/app/services/observ/prompt_manager/caching.rb +167 -0
- data/app/services/observ/prompt_manager/comparison.rb +49 -0
- data/app/services/observ/prompt_manager/version_management.rb +96 -0
- data/app/services/observ/prompt_manager.rb +40 -0
- data/app/services/observ/trace_text_formatter.rb +349 -0
- data/app/validators/observ/prompt_config_validator.rb +187 -0
- data/app/views/kaminari/_first_page.html.erb +11 -0
- data/app/views/kaminari/_gap.html.erb +8 -0
- data/app/views/kaminari/_last_page.html.erb +11 -0
- data/app/views/kaminari/_next_page.html.erb +11 -0
- data/app/views/kaminari/_page.html.erb +12 -0
- data/app/views/kaminari/_paginator.html.erb +25 -0
- data/app/views/kaminari/_prev_page.html.erb +11 -0
- data/app/views/kaminari/observ/_first_page.html.erb +11 -0
- data/app/views/kaminari/observ/_gap.html.erb +8 -0
- data/app/views/kaminari/observ/_last_page.html.erb +11 -0
- data/app/views/kaminari/observ/_next_page.html.erb +11 -0
- data/app/views/kaminari/observ/_page.html.erb +12 -0
- data/app/views/kaminari/observ/_paginator.html.erb +25 -0
- data/app/views/kaminari/observ/_prev_page.html.erb +11 -0
- data/app/views/layouts/observ/application.html.erb +88 -0
- data/app/views/observ/annotations/_annotation.html.erb +13 -0
- data/app/views/observ/annotations/_form.html.erb +28 -0
- data/app/views/observ/annotations/index.html.erb +28 -0
- data/app/views/observ/annotations/sessions_index.html.erb +48 -0
- data/app/views/observ/annotations/traces_index.html.erb +48 -0
- data/app/views/observ/chats/_form.html.erb +45 -0
- data/app/views/observ/chats/index.html.erb +67 -0
- data/app/views/observ/chats/new.html.erb +17 -0
- data/app/views/observ/chats/show.html.erb +34 -0
- data/app/views/observ/dashboard/index.html.erb +236 -0
- data/app/views/observ/dataset_items/_form.html.erb +49 -0
- data/app/views/observ/dataset_items/edit.html.erb +18 -0
- data/app/views/observ/dataset_items/index.html.erb +95 -0
- data/app/views/observ/dataset_items/new.html.erb +18 -0
- data/app/views/observ/dataset_run_items/_score_close_drawer.html.erb +4 -0
- data/app/views/observ/dataset_run_items/_score_drawer.html.erb +75 -0
- data/app/views/observ/dataset_run_items/_score_success.html.erb +29 -0
- data/app/views/observ/dataset_run_items/_scores_cell.html.erb +19 -0
- data/app/views/observ/dataset_run_items/details_drawer.turbo_stream.erb +80 -0
- data/app/views/observ/dataset_run_items/score_drawer.turbo_stream.erb +7 -0
- data/app/views/observ/dataset_runs/index.html.erb +108 -0
- data/app/views/observ/dataset_runs/new.html.erb +57 -0
- data/app/views/observ/dataset_runs/review.html.erb +155 -0
- data/app/views/observ/dataset_runs/show.html.erb +166 -0
- data/app/views/observ/datasets/_form.html.erb +62 -0
- data/app/views/observ/datasets/_items_tab.html.erb +66 -0
- data/app/views/observ/datasets/_runs_tab.html.erb +82 -0
- data/app/views/observ/datasets/edit.html.erb +32 -0
- data/app/views/observ/datasets/index.html.erb +105 -0
- data/app/views/observ/datasets/new.html.erb +18 -0
- data/app/views/observ/datasets/show.html.erb +67 -0
- data/app/views/observ/messages/_content.html.erb +1 -0
- data/app/views/observ/messages/_form.html.erb +33 -0
- data/app/views/observ/messages/_message.html.erb +14 -0
- data/app/views/observ/messages/_tool_calls.html.erb +10 -0
- data/app/views/observ/messages/create.turbo_stream.erb +9 -0
- data/app/views/observ/observations/index.html.erb +97 -0
- data/app/views/observ/observations/show_generation.html.erb +195 -0
- data/app/views/observ/observations/show_span.html.erb +93 -0
- data/app/views/observ/prompts/_diff_content.html.erb +16 -0
- data/app/views/observ/prompts/_form.html.erb +111 -0
- data/app/views/observ/prompts/_new_form.html.erb +102 -0
- data/app/views/observ/prompts/_prompt_actions.html.erb +4 -0
- data/app/views/observ/prompts/_prompt_content_highlighted.html.erb +4 -0
- data/app/views/observ/prompts/_version_actions.html.erb +40 -0
- data/app/views/observ/prompts/compare.html.erb +155 -0
- data/app/views/observ/prompts/edit.html.erb +17 -0
- data/app/views/observ/prompts/index.html.erb +108 -0
- data/app/views/observ/prompts/new.html.erb +17 -0
- data/app/views/observ/prompts/show.html.erb +138 -0
- data/app/views/observ/prompts/versions.html.erb +87 -0
- data/app/views/observ/sessions/annotations_drawer.turbo_stream.erb +25 -0
- data/app/views/observ/sessions/drawer_test.turbo_stream.erb +49 -0
- data/app/views/observ/sessions/index.html.erb +91 -0
- data/app/views/observ/sessions/show.html.erb +251 -0
- data/app/views/observ/traces/add_to_dataset_drawer.turbo_stream.erb +48 -0
- data/app/views/observ/traces/annotations_drawer.turbo_stream.erb +25 -0
- data/app/views/observ/traces/index.html.erb +87 -0
- data/app/views/observ/traces/show.html.erb +285 -0
- data/app/views/observ/traces/text_output_drawer.turbo_stream.erb +48 -0
- data/app/views/shared/_drawer.html.erb +26 -0
- data/config/routes.rb +80 -0
- data/db/migrate/001_create_observ_sessions.rb +21 -0
- data/db/migrate/002_create_observ_traces.rb +25 -0
- data/db/migrate/003_create_observ_observations.rb +42 -0
- data/db/migrate/004_add_message_id_to_observ_traces.rb +7 -0
- data/db/migrate/005_create_observ_prompts.rb +21 -0
- data/db/migrate/006_fix_prompt_config_strings.rb +23 -0
- data/db/migrate/007_create_observ_annotations.rb +12 -0
- data/db/migrate/009_add_prompt_fields_to_observ_chats.rb +11 -0
- data/db/migrate/010_create_observ_datasets.rb +15 -0
- data/db/migrate/011_create_observ_dataset_items.rb +17 -0
- data/db/migrate/012_create_observ_dataset_runs.rb +22 -0
- data/db/migrate/013_create_observ_dataset_run_items.rb +16 -0
- data/db/migrate/014_create_observ_scores.rb +26 -0
- data/lib/generators/observ/add_phase_tracking/add_phase_tracking_generator.rb +150 -0
- data/lib/generators/observ/add_phase_tracking/templates/migration.rb.tt +6 -0
- data/lib/generators/observ/install/USAGE +27 -0
- data/lib/generators/observ/install/install_generator.rb +270 -0
- data/lib/generators/observ/install_chat/install_chat_generator.rb +313 -0
- data/lib/generators/observ/install_chat/templates/agents/base_agent.rb.tt +147 -0
- data/lib/generators/observ/install_chat/templates/agents/simple_agent.rb.tt +55 -0
- data/lib/generators/observ/install_chat/templates/concerns/observ_chat_enhancements.rb.tt +34 -0
- data/lib/generators/observ/install_chat/templates/concerns/observ_message_enhancements.rb.tt +18 -0
- data/lib/generators/observ/install_chat/templates/initializers/observability.rb.tt +20 -0
- data/lib/generators/observ/install_chat/templates/jobs/chat_response_job.rb.tt +56 -0
- data/lib/generators/observ/install_chat/templates/migrations/add_agent_class_name.rb.tt +6 -0
- data/lib/generators/observ/install_chat/templates/migrations/add_observability_session_id.rb.tt +6 -0
- data/lib/generators/observ/install_chat/templates/tools/think_tool.rb.tt +29 -0
- data/lib/generators/observ/install_chat/templates/views/messages/_content.html.erb.tt +1 -0
- data/lib/observ/asset_installer.rb +130 -0
- data/lib/observ/asset_syncer.rb +104 -0
- data/lib/observ/configuration.rb +108 -0
- data/lib/observ/engine.rb +50 -0
- data/lib/observ/index_file_generator.rb +142 -0
- data/lib/observ/instrumenter/ruby_llm.rb +6 -0
- data/lib/observ/version.rb +3 -0
- data/lib/observ.rb +29 -0
- data/lib/tasks/observ_tasks.rake +75 -0
- metadata +453 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateObservDatasetRunItems < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :observ_dataset_run_items do |t|
|
|
6
|
+
t.references :dataset_run, null: false, foreign_key: { to_table: :observ_dataset_runs }
|
|
7
|
+
t.references :dataset_item, null: false, foreign_key: { to_table: :observ_dataset_items }
|
|
8
|
+
t.references :trace, foreign_key: { to_table: :observ_traces }
|
|
9
|
+
t.references :observation, foreign_key: { to_table: :observ_observations }
|
|
10
|
+
t.text :error
|
|
11
|
+
t.timestamps
|
|
12
|
+
|
|
13
|
+
t.index [ :dataset_run_id, :dataset_item_id ], unique: true, name: "idx_run_items_on_run_and_item"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateObservScores < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :observ_scores do |t|
|
|
6
|
+
t.references :dataset_run_item, null: false, foreign_key: { to_table: :observ_dataset_run_items }
|
|
7
|
+
t.references :trace, null: false, foreign_key: { to_table: :observ_traces }
|
|
8
|
+
t.references :observation, foreign_key: { to_table: :observ_observations }
|
|
9
|
+
|
|
10
|
+
t.string :name, null: false
|
|
11
|
+
t.decimal :value, precision: 10, scale: 4, null: false
|
|
12
|
+
t.integer :data_type, default: 0, null: false
|
|
13
|
+
t.integer :source, default: 0, null: false
|
|
14
|
+
|
|
15
|
+
t.text :comment
|
|
16
|
+
t.string :string_value
|
|
17
|
+
t.string :created_by
|
|
18
|
+
|
|
19
|
+
t.timestamps
|
|
20
|
+
|
|
21
|
+
t.index [ :dataset_run_item_id, :name, :source ], unique: true, name: "idx_scores_on_run_item_name_source"
|
|
22
|
+
t.index [ :trace_id, :name ]
|
|
23
|
+
t.index :name
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Observ
|
|
7
|
+
module Generators
|
|
8
|
+
# Generator for adding phase tracking to Observ chats
|
|
9
|
+
#
|
|
10
|
+
# This generator adds the ability to track multi-phase agent workflows
|
|
11
|
+
# by adding a current_phase column and including the AgentPhaseable concern.
|
|
12
|
+
#
|
|
13
|
+
# Prerequisites:
|
|
14
|
+
# - Observ chat feature already installed (rails generate observ:install:chat)
|
|
15
|
+
# - Chat model exists at app/models/chat.rb
|
|
16
|
+
# - ObservChatEnhancements concern exists
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# rails generate observ:add_phase_tracking
|
|
20
|
+
#
|
|
21
|
+
# What it does:
|
|
22
|
+
# 1. Adds current_phase column to chats table
|
|
23
|
+
# 2. Includes Observ::AgentPhaseable in ObservChatEnhancements
|
|
24
|
+
#
|
|
25
|
+
class AddPhaseTrackingGenerator < Rails::Generators::Base
|
|
26
|
+
include ActiveRecord::Generators::Migration
|
|
27
|
+
|
|
28
|
+
source_root File.expand_path("templates", __dir__)
|
|
29
|
+
|
|
30
|
+
def check_prerequisites
|
|
31
|
+
say "\n"
|
|
32
|
+
say "=" * 80, :cyan
|
|
33
|
+
say "Adding Phase Tracking to Observ Chats", :cyan
|
|
34
|
+
say "=" * 80, :cyan
|
|
35
|
+
say "\n"
|
|
36
|
+
|
|
37
|
+
check_chat_model_exists
|
|
38
|
+
check_concern_exists
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def create_migration
|
|
42
|
+
say "Creating migration for current_phase column...", :cyan
|
|
43
|
+
say "-" * 80, :cyan
|
|
44
|
+
|
|
45
|
+
migration_template "migration.rb.tt",
|
|
46
|
+
"db/migrate/add_phase_tracking_to_chats.rb"
|
|
47
|
+
|
|
48
|
+
say " ✓ Created migration for current_phase column", :green
|
|
49
|
+
say "\n"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def update_chat_enhancements
|
|
53
|
+
say "Updating ObservChatEnhancements concern...", :cyan
|
|
54
|
+
say "-" * 80, :cyan
|
|
55
|
+
|
|
56
|
+
concern_path = Rails.root.join("app/models/concerns/observ_chat_enhancements.rb")
|
|
57
|
+
concern_content = File.read(concern_path)
|
|
58
|
+
|
|
59
|
+
if concern_content.include?("Observ::AgentPhaseable")
|
|
60
|
+
say " ⚠ AgentPhaseable already included in ObservChatEnhancements", :yellow
|
|
61
|
+
else
|
|
62
|
+
inject_into_file concern_path,
|
|
63
|
+
after: "include Observ::ObservabilityInstrumentation\n" do
|
|
64
|
+
" include Observ::AgentPhaseable\n"
|
|
65
|
+
end
|
|
66
|
+
say " ✓ Included AgentPhaseable in ObservChatEnhancements", :green
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
say "\n"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def show_post_install_instructions
|
|
73
|
+
say "\n"
|
|
74
|
+
say "=" * 80, :green
|
|
75
|
+
say "Phase Tracking Installation Complete!", :green
|
|
76
|
+
say "=" * 80, :green
|
|
77
|
+
say "\n"
|
|
78
|
+
|
|
79
|
+
say "Next steps:", :cyan
|
|
80
|
+
say "\n"
|
|
81
|
+
|
|
82
|
+
say "1. Run migrations:", :cyan
|
|
83
|
+
say " rails db:migrate", :white
|
|
84
|
+
say "\n"
|
|
85
|
+
|
|
86
|
+
say "2. (Optional) Define allowed phases in your Chat model:", :cyan
|
|
87
|
+
say " # app/models/chat.rb", :white
|
|
88
|
+
say " class Chat < ApplicationRecord", :white
|
|
89
|
+
say " # ...", :white
|
|
90
|
+
say " def allowed_phases", :white
|
|
91
|
+
say " %w[scoping research writing review]", :white
|
|
92
|
+
say " end", :white
|
|
93
|
+
say " end", :white
|
|
94
|
+
say "\n"
|
|
95
|
+
|
|
96
|
+
say "3. Use phase transitions in your agents:", :cyan
|
|
97
|
+
say " chat.transition_to_phase('research')", :white
|
|
98
|
+
say " chat.in_phase?('research') # => true", :white
|
|
99
|
+
say " chat.current_phase # => 'research'", :white
|
|
100
|
+
say "\n"
|
|
101
|
+
|
|
102
|
+
say "Documentation:", :cyan
|
|
103
|
+
say " • See app/models/concerns/observ/agent_phaseable.rb for full API", :white
|
|
104
|
+
say "\n"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def check_chat_model_exists
|
|
110
|
+
unless File.exist?(Rails.root.join("app/models/chat.rb"))
|
|
111
|
+
raise Thor::Error, <<~ERROR
|
|
112
|
+
Chat model not found!
|
|
113
|
+
|
|
114
|
+
This generator requires the Chat model to exist.
|
|
115
|
+
|
|
116
|
+
Please run:
|
|
117
|
+
rails generate observ:install:chat
|
|
118
|
+
rails db:migrate
|
|
119
|
+
#{' '}
|
|
120
|
+
Then run this generator again.
|
|
121
|
+
ERROR
|
|
122
|
+
end
|
|
123
|
+
say " ✓ Chat model found", :green
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def check_concern_exists
|
|
127
|
+
concern_path = Rails.root.join("app/models/concerns/observ_chat_enhancements.rb")
|
|
128
|
+
unless File.exist?(concern_path)
|
|
129
|
+
raise Thor::Error, <<~ERROR
|
|
130
|
+
ObservChatEnhancements concern not found!
|
|
131
|
+
|
|
132
|
+
This generator requires observ:install:chat to be run first.
|
|
133
|
+
|
|
134
|
+
Please run:
|
|
135
|
+
rails generate observ:install:chat
|
|
136
|
+
rails db:migrate
|
|
137
|
+
#{' '}
|
|
138
|
+
Then run this generator again.
|
|
139
|
+
ERROR
|
|
140
|
+
end
|
|
141
|
+
say " ✓ ObservChatEnhancements concern found", :green
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Helper for migration timestamps
|
|
145
|
+
def migration_version
|
|
146
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Install Observ assets (stylesheets and JavaScript controllers) in your Rails application.
|
|
3
|
+
|
|
4
|
+
This generator will:
|
|
5
|
+
- Copy Observ stylesheets to your app
|
|
6
|
+
- Copy Observ JavaScript Stimulus controllers to your app
|
|
7
|
+
- Generate index files for easy importing
|
|
8
|
+
- Check if controllers are properly registered
|
|
9
|
+
|
|
10
|
+
Examples:
|
|
11
|
+
rails generate observ:install
|
|
12
|
+
|
|
13
|
+
This will install assets to default locations:
|
|
14
|
+
- Styles: app/javascript/stylesheets/observ
|
|
15
|
+
- JavaScript: app/javascript/controllers/observ
|
|
16
|
+
|
|
17
|
+
rails generate observ:install --styles-dest=app/assets/stylesheets/observ
|
|
18
|
+
|
|
19
|
+
This will install stylesheets to a custom location
|
|
20
|
+
|
|
21
|
+
rails generate observ:install --js-dest=app/javascript/controllers/custom_observ
|
|
22
|
+
|
|
23
|
+
This will install JavaScript controllers to a custom location
|
|
24
|
+
|
|
25
|
+
rails generate observ:install --skip-index
|
|
26
|
+
|
|
27
|
+
This will skip generation of index files (useful if you want to manage imports manually)
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "observ/asset_installer"
|
|
5
|
+
|
|
6
|
+
module Observ
|
|
7
|
+
module Generators
|
|
8
|
+
# Generator for installing Observ assets in a Rails application
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# rails generate observ:install
|
|
12
|
+
# rails generate observ:install --styles-dest=custom/path
|
|
13
|
+
# rails generate observ:install --js-dest=custom/path
|
|
14
|
+
# rails generate observ:install --skip-index
|
|
15
|
+
# rails generate observ:install --skip-routes # Don't auto-mount engine
|
|
16
|
+
# rails generate observ:install --force # Skip confirmation prompt
|
|
17
|
+
class InstallGenerator < Rails::Generators::Base
|
|
18
|
+
source_root File.expand_path("templates", __dir__)
|
|
19
|
+
|
|
20
|
+
class_option :styles_dest,
|
|
21
|
+
type: :string,
|
|
22
|
+
desc: "Destination path for stylesheets (default: app/javascript/stylesheets/observ)",
|
|
23
|
+
default: nil
|
|
24
|
+
|
|
25
|
+
class_option :js_dest,
|
|
26
|
+
type: :string,
|
|
27
|
+
desc: "Destination path for JavaScript controllers (default: app/javascript/controllers/observ)",
|
|
28
|
+
default: nil
|
|
29
|
+
|
|
30
|
+
class_option :skip_index,
|
|
31
|
+
type: :boolean,
|
|
32
|
+
desc: "Skip generation of index files",
|
|
33
|
+
default: false
|
|
34
|
+
|
|
35
|
+
class_option :force,
|
|
36
|
+
type: :boolean,
|
|
37
|
+
desc: "Skip confirmation prompt",
|
|
38
|
+
default: false
|
|
39
|
+
|
|
40
|
+
class_option :skip_routes,
|
|
41
|
+
type: :boolean,
|
|
42
|
+
desc: "Skip automatic route mounting",
|
|
43
|
+
default: false
|
|
44
|
+
|
|
45
|
+
def confirm_installation
|
|
46
|
+
return if options[:force]
|
|
47
|
+
|
|
48
|
+
styles_dest = options[:styles_dest] || Observ::AssetInstaller::DEFAULT_STYLES_DEST
|
|
49
|
+
js_dest = options[:js_dest] || Observ::AssetInstaller::DEFAULT_JS_DEST
|
|
50
|
+
|
|
51
|
+
say "\n"
|
|
52
|
+
say "=" * 80, :cyan
|
|
53
|
+
say "Observ Installation", :cyan
|
|
54
|
+
say "=" * 80, :cyan
|
|
55
|
+
say "\n"
|
|
56
|
+
|
|
57
|
+
# Collect files that will actually be copied
|
|
58
|
+
stylesheets_to_copy = collect_files_to_copy("stylesheets", "*.scss", styles_dest)
|
|
59
|
+
js_files_to_copy = collect_files_to_copy("javascripts", "*.js", js_dest)
|
|
60
|
+
index_will_be_generated = !options[:skip_index] && will_generate_index?(js_dest)
|
|
61
|
+
route_will_be_added = !options[:skip_routes] && !route_already_exists?
|
|
62
|
+
|
|
63
|
+
# Check if there are any changes to make
|
|
64
|
+
total_changes = stylesheets_to_copy.count + js_files_to_copy.count +
|
|
65
|
+
(index_will_be_generated ? 1 : 0) + (route_will_be_added ? 1 : 0)
|
|
66
|
+
|
|
67
|
+
if total_changes == 0
|
|
68
|
+
say "No changes needed - all files are up to date!", :green
|
|
69
|
+
say "\n"
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
say "The following changes will be made:", :yellow
|
|
74
|
+
say "\n"
|
|
75
|
+
|
|
76
|
+
# Show stylesheet files that will be copied
|
|
77
|
+
if stylesheets_to_copy.any?
|
|
78
|
+
say "Stylesheets (#{stylesheets_to_copy.count} files to #{styles_dest}):", :yellow
|
|
79
|
+
stylesheets_to_copy.each do |file|
|
|
80
|
+
say " • #{File.basename(file)}", :white
|
|
81
|
+
end
|
|
82
|
+
say "\n"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Show JavaScript files that will be copied
|
|
86
|
+
if js_files_to_copy.any?
|
|
87
|
+
say "JavaScript Controllers (#{js_files_to_copy.count} files to #{js_dest}):", :yellow
|
|
88
|
+
js_files_to_copy.each do |file|
|
|
89
|
+
say " • #{File.basename(file)}", :white
|
|
90
|
+
end
|
|
91
|
+
say "\n"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Show generated files
|
|
95
|
+
if index_will_be_generated
|
|
96
|
+
say "Generated Files:", :yellow
|
|
97
|
+
say " • #{js_dest}/index.js (controller index)", :white
|
|
98
|
+
say "\n"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Show routes
|
|
102
|
+
if route_will_be_added
|
|
103
|
+
say "Routes (will be added to config/routes.rb):", :yellow
|
|
104
|
+
say ' mount Observ::Engine, at: "/observ"', :white
|
|
105
|
+
say "\n"
|
|
106
|
+
elsif !options[:skip_routes]
|
|
107
|
+
say "Routes:", :green
|
|
108
|
+
say " Engine already mounted in config/routes.rb", :green
|
|
109
|
+
say "\n"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
unless yes?("Do you want to proceed with the installation? (y/n)", :yellow)
|
|
113
|
+
say "\nInstallation cancelled.", :red
|
|
114
|
+
exit 0
|
|
115
|
+
end
|
|
116
|
+
say "\n"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def mount_engine
|
|
120
|
+
return if options[:skip_routes]
|
|
121
|
+
|
|
122
|
+
say "Checking routes...", :cyan
|
|
123
|
+
say "-" * 80, :cyan
|
|
124
|
+
|
|
125
|
+
if route_already_exists?
|
|
126
|
+
say " Engine already mounted in config/routes.rb", :yellow
|
|
127
|
+
else
|
|
128
|
+
route 'mount Observ::Engine, at: "/observ"'
|
|
129
|
+
say " ✓ Added route: mount Observ::Engine, at: \"/observ\"", :green
|
|
130
|
+
end
|
|
131
|
+
say "\n"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def install_assets
|
|
135
|
+
installer = Observ::AssetInstaller.new(
|
|
136
|
+
gem_root: Observ::Engine.root,
|
|
137
|
+
app_root: Rails.root,
|
|
138
|
+
logger: GeneratorLogger.new(self)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
@result = installer.install(
|
|
142
|
+
styles_dest: options[:styles_dest],
|
|
143
|
+
js_dest: options[:js_dest],
|
|
144
|
+
generate_index: !options[:skip_index]
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def show_post_install_message
|
|
149
|
+
say "\n"
|
|
150
|
+
say "=" * 80, :green
|
|
151
|
+
say "Observ installed successfully!", :green
|
|
152
|
+
say "=" * 80, :green
|
|
153
|
+
say "\n"
|
|
154
|
+
|
|
155
|
+
if @result[:registration] && @result[:registration][:suggestions]
|
|
156
|
+
say "⚠ Action required:", :yellow
|
|
157
|
+
@result[:registration][:suggestions].each do |suggestion|
|
|
158
|
+
say " #{suggestion}", :yellow
|
|
159
|
+
end
|
|
160
|
+
say "\n"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
say "Next steps:", :cyan
|
|
164
|
+
say " 1. Import stylesheets in your application", :cyan
|
|
165
|
+
say " Add to app/javascript/application.js:", :cyan
|
|
166
|
+
say " import 'observ'", :cyan
|
|
167
|
+
say "\n"
|
|
168
|
+
say " 2. Restart your development server", :cyan
|
|
169
|
+
say " bin/dev or rails server", :cyan
|
|
170
|
+
say "\n"
|
|
171
|
+
say " 3. Visit /observ in your browser", :cyan
|
|
172
|
+
unless options[:skip_routes]
|
|
173
|
+
say " (Engine mounted at /observ)", :cyan
|
|
174
|
+
end
|
|
175
|
+
say "\n"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def route_already_exists?
|
|
181
|
+
routes_file = Rails.root.join("config/routes.rb")
|
|
182
|
+
return false unless routes_file.exist?
|
|
183
|
+
|
|
184
|
+
routes_content = File.read(routes_file)
|
|
185
|
+
routes_content.match?(/mount\s+Observ::Engine/)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Collect only files that will actually be copied (new or modified)
|
|
189
|
+
# @param asset_type [String] "stylesheets" or "javascripts"
|
|
190
|
+
# @param pattern [String] File glob pattern (e.g., "*.scss", "*.js")
|
|
191
|
+
# @param dest_path [String] Destination directory path
|
|
192
|
+
# @return [Array<String>] List of source file paths that will be copied
|
|
193
|
+
def collect_files_to_copy(asset_type, pattern, dest_path)
|
|
194
|
+
source_path = get_source_path(asset_type)
|
|
195
|
+
return [] unless source_path.directory?
|
|
196
|
+
|
|
197
|
+
dest_path = Rails.root.join(dest_path)
|
|
198
|
+
|
|
199
|
+
files_to_copy = []
|
|
200
|
+
Dir.glob(source_path.join(pattern)).sort.each do |source_file|
|
|
201
|
+
filename = File.basename(source_file)
|
|
202
|
+
dest_file = dest_path.join(filename)
|
|
203
|
+
|
|
204
|
+
if should_copy_file?(source_file, dest_file)
|
|
205
|
+
files_to_copy << source_file
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
files_to_copy
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Get the source path for the given asset type
|
|
213
|
+
# @param asset_type [String] "stylesheets" or "javascripts"
|
|
214
|
+
# @return [Pathname] Source directory path
|
|
215
|
+
def get_source_path(asset_type)
|
|
216
|
+
if asset_type == "stylesheets"
|
|
217
|
+
Observ::Engine.root.join("app", "assets", "stylesheets", "observ")
|
|
218
|
+
else
|
|
219
|
+
Observ::Engine.root.join("app", "assets", "javascripts", "observ", "controllers")
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Determine if a file should be copied (matches AssetSyncer logic)
|
|
224
|
+
# @param source_file [String] Path to source file
|
|
225
|
+
# @param dest_file [Pathname] Path to destination file
|
|
226
|
+
# @return [Boolean] true if file should be copied
|
|
227
|
+
def should_copy_file?(source_file, dest_file)
|
|
228
|
+
!dest_file.exist? || !FileUtils.identical?(source_file, dest_file.to_s)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Check if index.js will be generated (new or different content)
|
|
232
|
+
# @param js_dest [String] Destination path for JavaScript controllers
|
|
233
|
+
# @return [Boolean] true if index.js will be generated
|
|
234
|
+
def will_generate_index?(js_dest)
|
|
235
|
+
index_file = Rails.root.join(js_dest, "index.js")
|
|
236
|
+
# Index file will be generated if it doesn't exist
|
|
237
|
+
# (we don't check content as the generator always creates it)
|
|
238
|
+
!index_file.exist?
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Logger adapter for Rails generator
|
|
242
|
+
class GeneratorLogger
|
|
243
|
+
def initialize(generator)
|
|
244
|
+
@generator = generator
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def puts(message)
|
|
248
|
+
# Remove color codes and special characters for cleaner output
|
|
249
|
+
clean_message = message.gsub(/[✓✗⚠-]/, "").strip
|
|
250
|
+
|
|
251
|
+
if message.include?("✓") || message.include?("Copied")
|
|
252
|
+
@generator.say(" #{clean_message}", :green)
|
|
253
|
+
elsif message.include?("⚠")
|
|
254
|
+
@generator.say(" #{clean_message}", :yellow)
|
|
255
|
+
elsif message.include?("=") || message.start_with?("Syncing", "Generating", "Checking")
|
|
256
|
+
@generator.say(clean_message, :cyan)
|
|
257
|
+
elsif message.include?("Skipped")
|
|
258
|
+
@generator.say(" #{clean_message}", :white)
|
|
259
|
+
else
|
|
260
|
+
@generator.say(clean_message)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def info(message)
|
|
265
|
+
puts(message)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|