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,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Observ
|
|
4
|
+
# Main service class for installing Observ assets in a Rails application
|
|
5
|
+
class AssetInstaller
|
|
6
|
+
attr_reader :gem_root, :app_root, :logger
|
|
7
|
+
|
|
8
|
+
DEFAULT_STYLES_DEST = "app/javascript/stylesheets/observ"
|
|
9
|
+
DEFAULT_JS_DEST = "app/javascript/controllers/observ"
|
|
10
|
+
|
|
11
|
+
# @param gem_root [String, Pathname] Root directory of the gem
|
|
12
|
+
# @param app_root [String, Pathname] Root directory of the host application
|
|
13
|
+
# @param logger [Logger, IO] Logger for output (defaults to STDOUT)
|
|
14
|
+
def initialize(gem_root:, app_root:, logger: $stdout)
|
|
15
|
+
@gem_root = Pathname.new(gem_root)
|
|
16
|
+
@app_root = Pathname.new(app_root)
|
|
17
|
+
@logger = logger
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Install assets with custom or default destinations
|
|
21
|
+
# @param styles_dest [String, nil] Custom destination for stylesheets
|
|
22
|
+
# @param js_dest [String, nil] Custom destination for JavaScript controllers
|
|
23
|
+
# @param generate_index [Boolean] Whether to generate index files
|
|
24
|
+
# @return [Hash] Installation results
|
|
25
|
+
def install(styles_dest: nil, js_dest: nil, generate_index: true)
|
|
26
|
+
styles_dest ||= DEFAULT_STYLES_DEST
|
|
27
|
+
js_dest ||= DEFAULT_JS_DEST
|
|
28
|
+
|
|
29
|
+
styles_dest_path = app_root.join(styles_dest)
|
|
30
|
+
js_dest_path = app_root.join(js_dest)
|
|
31
|
+
|
|
32
|
+
log_header(styles_dest_path, js_dest_path)
|
|
33
|
+
|
|
34
|
+
syncer = AssetSyncer.new(gem_root: gem_root, app_root: app_root, logger: logger)
|
|
35
|
+
|
|
36
|
+
# Sync stylesheets
|
|
37
|
+
styles_result = syncer.sync_stylesheets(styles_dest_path)
|
|
38
|
+
log ""
|
|
39
|
+
|
|
40
|
+
# Sync JavaScript controllers
|
|
41
|
+
js_result = syncer.sync_javascript_controllers(js_dest_path)
|
|
42
|
+
log ""
|
|
43
|
+
|
|
44
|
+
# Check controller registration if requested
|
|
45
|
+
# Note: index.js is already included in the gem's source files and gets synced,
|
|
46
|
+
# so we don't need to generate it - we just check if it's properly registered
|
|
47
|
+
registration_status = nil
|
|
48
|
+
|
|
49
|
+
if generate_index
|
|
50
|
+
generator = IndexFileGenerator.new(app_root: app_root, logger: logger)
|
|
51
|
+
|
|
52
|
+
log "Checking controller registration..."
|
|
53
|
+
log "-" * 80
|
|
54
|
+
registration_status = generator.check_main_controllers_registration
|
|
55
|
+
|
|
56
|
+
if registration_status[:suggestions]
|
|
57
|
+
registration_status[:suggestions].each { |msg| log " #{msg}" }
|
|
58
|
+
elsif registration_status[:registered]
|
|
59
|
+
log " ✓ Observ controllers are already registered"
|
|
60
|
+
end
|
|
61
|
+
log ""
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
log_footer(styles_dest_path, js_dest_path)
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
styles: styles_result,
|
|
68
|
+
javascript: js_result,
|
|
69
|
+
registration: registration_status,
|
|
70
|
+
paths: {
|
|
71
|
+
styles: styles_dest_path,
|
|
72
|
+
javascript: js_dest_path
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Sync existing assets (update only)
|
|
78
|
+
# @param styles_dest [String, nil] Custom destination for stylesheets
|
|
79
|
+
# @param js_dest [String, nil] Custom destination for JavaScript controllers
|
|
80
|
+
# @return [Hash] Sync results
|
|
81
|
+
def sync(styles_dest: nil, js_dest: nil)
|
|
82
|
+
install(styles_dest: styles_dest, js_dest: js_dest, generate_index: false)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Log the header with configuration info
|
|
88
|
+
def log_header(styles_dest_path, js_dest_path)
|
|
89
|
+
log "=" * 80
|
|
90
|
+
log "Observ Asset Installation"
|
|
91
|
+
log "=" * 80
|
|
92
|
+
log ""
|
|
93
|
+
log "Gem location: #{gem_root}"
|
|
94
|
+
log "App location: #{app_root}"
|
|
95
|
+
log ""
|
|
96
|
+
log "Destinations:"
|
|
97
|
+
log " Styles: #{styles_dest_path.relative_path_from(app_root)}"
|
|
98
|
+
log " JavaScript: #{js_dest_path.relative_path_from(app_root)}"
|
|
99
|
+
log ""
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Log the footer with next steps
|
|
103
|
+
def log_footer(styles_dest_path, js_dest_path)
|
|
104
|
+
log "=" * 80
|
|
105
|
+
log "✓ Asset installation complete!"
|
|
106
|
+
log "=" * 80
|
|
107
|
+
log ""
|
|
108
|
+
log "Installed to:"
|
|
109
|
+
log " Styles: #{styles_dest_path.relative_path_from(app_root)}"
|
|
110
|
+
log " JavaScript: #{js_dest_path.relative_path_from(app_root)}"
|
|
111
|
+
log ""
|
|
112
|
+
log "Next steps:"
|
|
113
|
+
log " 1. Import the stylesheets in your application.scss or application.js"
|
|
114
|
+
log " 2. Ensure the controllers index imports './observ' (see above)"
|
|
115
|
+
log " 3. Restart your dev server (bin/dev)"
|
|
116
|
+
log " 4. Verify assets are loaded correctly"
|
|
117
|
+
log ""
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Log a message
|
|
121
|
+
# @param message [String] Message to log
|
|
122
|
+
def log(message)
|
|
123
|
+
if logger.respond_to?(:puts)
|
|
124
|
+
logger.puts(message)
|
|
125
|
+
else
|
|
126
|
+
logger.info(message)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Observ
|
|
4
|
+
# Service class for syncing assets from the gem to the host application
|
|
5
|
+
class AssetSyncer
|
|
6
|
+
attr_reader :gem_root, :app_root, :logger
|
|
7
|
+
|
|
8
|
+
# @param gem_root [String, Pathname] Root directory of the gem
|
|
9
|
+
# @param app_root [String, Pathname] Root directory of the host application
|
|
10
|
+
# @param logger [Logger, IO] Logger for output (defaults to STDOUT)
|
|
11
|
+
def initialize(gem_root:, app_root:, logger: $stdout)
|
|
12
|
+
@gem_root = Pathname.new(gem_root)
|
|
13
|
+
@app_root = Pathname.new(app_root)
|
|
14
|
+
@logger = logger
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Sync stylesheets to the destination path
|
|
18
|
+
# @param dest_path [String, Pathname] Destination directory
|
|
19
|
+
# @return [Hash] Statistics about the sync operation
|
|
20
|
+
def sync_stylesheets(dest_path)
|
|
21
|
+
source_path = gem_root.join("app", "assets", "stylesheets", "observ")
|
|
22
|
+
sync_files(
|
|
23
|
+
source_path: source_path,
|
|
24
|
+
dest_path: Pathname.new(dest_path),
|
|
25
|
+
pattern: "*.scss",
|
|
26
|
+
label: "stylesheets"
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Sync JavaScript controllers to the destination path
|
|
31
|
+
# @param dest_path [String, Pathname] Destination directory
|
|
32
|
+
# @return [Hash] Statistics about the sync operation
|
|
33
|
+
def sync_javascript_controllers(dest_path)
|
|
34
|
+
source_path = gem_root.join("app", "assets", "javascripts", "observ", "controllers")
|
|
35
|
+
sync_files(
|
|
36
|
+
source_path: source_path,
|
|
37
|
+
dest_path: Pathname.new(dest_path),
|
|
38
|
+
pattern: "*.js",
|
|
39
|
+
label: "JavaScript controllers"
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Sync files matching a pattern from source to destination
|
|
46
|
+
# @param source_path [Pathname] Source directory
|
|
47
|
+
# @param dest_path [Pathname] Destination directory
|
|
48
|
+
# @param pattern [String] File glob pattern
|
|
49
|
+
# @param label [String] Human-readable label for logging
|
|
50
|
+
# @return [Hash] Statistics about the sync operation
|
|
51
|
+
def sync_files(source_path:, dest_path:, pattern:, label:)
|
|
52
|
+
log "Syncing Observ #{label}..."
|
|
53
|
+
log "-" * 80
|
|
54
|
+
|
|
55
|
+
unless source_path.directory?
|
|
56
|
+
log " ⚠ Source #{label} directory not found: #{source_path}"
|
|
57
|
+
return { files_copied: 0, files_skipped: 0, error: "Source directory not found" }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Ensure destination directory exists
|
|
61
|
+
FileUtils.mkdir_p(dest_path)
|
|
62
|
+
|
|
63
|
+
files_copied = 0
|
|
64
|
+
files_skipped = 0
|
|
65
|
+
|
|
66
|
+
Dir.glob(source_path.join(pattern)).sort.each do |file|
|
|
67
|
+
filename = File.basename(file)
|
|
68
|
+
dest_file = dest_path.join(filename)
|
|
69
|
+
|
|
70
|
+
if should_copy_file?(file, dest_file)
|
|
71
|
+
FileUtils.cp(file, dest_file)
|
|
72
|
+
log " ✓ Copied #{filename}"
|
|
73
|
+
files_copied += 1
|
|
74
|
+
else
|
|
75
|
+
log " - Skipped #{filename} (no changes)"
|
|
76
|
+
files_skipped += 1
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
log ""
|
|
81
|
+
log " Total: #{files_copied} file(s) updated, #{files_skipped} file(s) skipped"
|
|
82
|
+
|
|
83
|
+
{ files_copied: files_copied, files_skipped: files_skipped }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Determine if a file should be copied
|
|
87
|
+
# @param source_file [String] Path to source file
|
|
88
|
+
# @param dest_file [Pathname] Path to destination file
|
|
89
|
+
# @return [Boolean] true if file should be copied
|
|
90
|
+
def should_copy_file?(source_file, dest_file)
|
|
91
|
+
!dest_file.exist? || !FileUtils.identical?(source_file, dest_file.to_s)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Log a message
|
|
95
|
+
# @param message [String] Message to log
|
|
96
|
+
def log(message)
|
|
97
|
+
if logger.respond_to?(:puts)
|
|
98
|
+
logger.puts(message)
|
|
99
|
+
else
|
|
100
|
+
logger.info(message)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Observ
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :prompt_management_enabled,
|
|
6
|
+
:prompt_cache_ttl,
|
|
7
|
+
:prompt_fallback_behavior,
|
|
8
|
+
:prompt_cache_store,
|
|
9
|
+
:prompt_cache_prefix,
|
|
10
|
+
:prompt_cache_namespace,
|
|
11
|
+
:prompt_max_versions,
|
|
12
|
+
:prompt_default_state,
|
|
13
|
+
:prompt_allow_production_deletion,
|
|
14
|
+
:prompt_cache_warming_enabled,
|
|
15
|
+
:prompt_cache_critical_prompts,
|
|
16
|
+
:prompt_cache_monitoring_enabled,
|
|
17
|
+
:prompt_config_schema,
|
|
18
|
+
:prompt_config_schema_strict,
|
|
19
|
+
:back_to_app_path,
|
|
20
|
+
:back_to_app_label,
|
|
21
|
+
:chat_ui_enabled,
|
|
22
|
+
:agent_path,
|
|
23
|
+
:pagination_per_page
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
@prompt_management_enabled = true
|
|
27
|
+
@prompt_cache_ttl = 300 # 5 minutes
|
|
28
|
+
@prompt_fallback_behavior = :raise # or :return_nil, :use_fallback
|
|
29
|
+
@prompt_cache_store = :redis_cache_store
|
|
30
|
+
@prompt_cache_prefix = "observ:prompt"
|
|
31
|
+
@prompt_cache_namespace = "observ:prompt"
|
|
32
|
+
@prompt_max_versions = 100
|
|
33
|
+
@prompt_default_state = :production
|
|
34
|
+
@prompt_allow_production_deletion = false
|
|
35
|
+
@prompt_cache_warming_enabled = true
|
|
36
|
+
@prompt_cache_critical_prompts = []
|
|
37
|
+
@prompt_cache_monitoring_enabled = true
|
|
38
|
+
@prompt_config_schema = default_prompt_config_schema
|
|
39
|
+
@prompt_config_schema_strict = false
|
|
40
|
+
@back_to_app_path = -> { "/" }
|
|
41
|
+
@back_to_app_label = "← Back to App"
|
|
42
|
+
@chat_ui_enabled = -> { defined?(::Chat) && ::Chat.respond_to?(:acts_as_chat) }
|
|
43
|
+
@agent_path = nil # Defaults to Rails.root.join("app", "agents")
|
|
44
|
+
@pagination_per_page = 25
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check if chat UI is enabled
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
def chat_ui_enabled?
|
|
50
|
+
return @chat_ui_enabled.call if @chat_ui_enabled.respond_to?(:call)
|
|
51
|
+
@chat_ui_enabled
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Default schema for prompt configuration validation
|
|
55
|
+
# @return [Hash]
|
|
56
|
+
def default_prompt_config_schema
|
|
57
|
+
{
|
|
58
|
+
temperature: {
|
|
59
|
+
type: :float,
|
|
60
|
+
required: false,
|
|
61
|
+
range: 0.0..2.0,
|
|
62
|
+
default: 0.7
|
|
63
|
+
},
|
|
64
|
+
max_tokens: {
|
|
65
|
+
type: :integer,
|
|
66
|
+
required: false,
|
|
67
|
+
range: 1..100000
|
|
68
|
+
},
|
|
69
|
+
top_p: {
|
|
70
|
+
type: :float,
|
|
71
|
+
required: false,
|
|
72
|
+
range: 0.0..1.0
|
|
73
|
+
},
|
|
74
|
+
frequency_penalty: {
|
|
75
|
+
type: :float,
|
|
76
|
+
required: false,
|
|
77
|
+
range: -2.0..2.0
|
|
78
|
+
},
|
|
79
|
+
presence_penalty: {
|
|
80
|
+
type: :float,
|
|
81
|
+
required: false,
|
|
82
|
+
range: -2.0..2.0
|
|
83
|
+
},
|
|
84
|
+
stop_sequences: {
|
|
85
|
+
type: :array,
|
|
86
|
+
required: false,
|
|
87
|
+
item_type: :string
|
|
88
|
+
},
|
|
89
|
+
model: {
|
|
90
|
+
type: :string,
|
|
91
|
+
required: false
|
|
92
|
+
},
|
|
93
|
+
response_format: {
|
|
94
|
+
type: :hash,
|
|
95
|
+
required: false
|
|
96
|
+
},
|
|
97
|
+
seed: {
|
|
98
|
+
type: :integer,
|
|
99
|
+
required: false
|
|
100
|
+
},
|
|
101
|
+
stream: {
|
|
102
|
+
type: :boolean,
|
|
103
|
+
required: false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Observ
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace Observ
|
|
4
|
+
|
|
5
|
+
config.generators do |g|
|
|
6
|
+
g.test_framework :rspec
|
|
7
|
+
g.fixture_replacement :factory_bot
|
|
8
|
+
g.factory_bot dir: "spec/factories"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Make concerns available to host app
|
|
12
|
+
initializer "observ.load_concerns" do
|
|
13
|
+
config.to_prepare do
|
|
14
|
+
Dir[Observ::Engine.root.join("app", "models", "concerns", "observ", "*.rb")].each do |concern|
|
|
15
|
+
require_dependency concern
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Asset configuration
|
|
21
|
+
initializer "observ.assets" do |app|
|
|
22
|
+
# Add engine assets to the asset pipeline
|
|
23
|
+
if app.config.respond_to?(:assets)
|
|
24
|
+
app.config.assets.paths << root.join("app/assets/stylesheets")
|
|
25
|
+
app.config.assets.paths << root.join("app/assets/javascripts")
|
|
26
|
+
app.config.assets.precompile += %w[ observ/application.css observ/application.js ]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Configure cache warming
|
|
31
|
+
initializer "observ.configure_cache" do |app|
|
|
32
|
+
config.after_initialize do
|
|
33
|
+
next unless Observ.config.prompt_cache_warming_enabled
|
|
34
|
+
|
|
35
|
+
# Warm cache asynchronously to avoid blocking boot
|
|
36
|
+
Thread.new do
|
|
37
|
+
sleep 2 # Wait for app to fully boot
|
|
38
|
+
begin
|
|
39
|
+
if defined?(Observ::PromptManager)
|
|
40
|
+
Observ::PromptManager.warm_cache(Observ.config.prompt_cache_critical_prompts)
|
|
41
|
+
Rails.logger.info "Observ cache warming completed"
|
|
42
|
+
end
|
|
43
|
+
rescue => e
|
|
44
|
+
Rails.logger.error "Observ cache warming failed: #{e.message}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Observ
|
|
4
|
+
# Service class for generating index files for Stimulus controllers
|
|
5
|
+
class IndexFileGenerator
|
|
6
|
+
attr_reader :app_root, :logger
|
|
7
|
+
|
|
8
|
+
# @param app_root [String, Pathname] Root directory of the host application
|
|
9
|
+
# @param logger [Logger, IO] Logger for output (defaults to STDOUT)
|
|
10
|
+
def initialize(app_root:, logger: $stdout)
|
|
11
|
+
@app_root = Pathname.new(app_root)
|
|
12
|
+
@logger = logger
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Generate or update the Observ controllers index file
|
|
16
|
+
# @param controllers_path [String, Pathname] Path to the controllers directory
|
|
17
|
+
# @return [Hash] Result of the operation
|
|
18
|
+
def generate_controllers_index(controllers_path)
|
|
19
|
+
controllers_path = Pathname.new(controllers_path)
|
|
20
|
+
index_file = controllers_path.join("index.js")
|
|
21
|
+
file_existed = index_file.exist?
|
|
22
|
+
|
|
23
|
+
# Find all controller files
|
|
24
|
+
controller_files = Dir.glob(controllers_path.join("*_controller.js")).sort
|
|
25
|
+
|
|
26
|
+
if controller_files.empty?
|
|
27
|
+
log " ⚠ No controller files found in #{controllers_path}"
|
|
28
|
+
return { created: false, error: "No controller files found" }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
content = generate_index_content(controller_files, controllers_path)
|
|
32
|
+
|
|
33
|
+
if file_existed
|
|
34
|
+
existing_content = File.read(index_file)
|
|
35
|
+
if existing_content == content
|
|
36
|
+
log " - Index file already up to date: #{index_file.relative_path_from(app_root)}"
|
|
37
|
+
return { created: false, updated: false, path: index_file }
|
|
38
|
+
else
|
|
39
|
+
log " ✓ Updated index file: #{index_file.relative_path_from(app_root)}"
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
log " ✓ Created index file: #{index_file.relative_path_from(app_root)}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
File.write(index_file, content)
|
|
46
|
+
{ created: !file_existed, updated: file_existed, path: index_file }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if Observ controllers are registered in the main controllers index
|
|
50
|
+
# @return [Hash] Registration status and suggestions
|
|
51
|
+
def check_main_controllers_registration
|
|
52
|
+
main_index = app_root.join("app", "javascript", "controllers", "index.js")
|
|
53
|
+
|
|
54
|
+
unless main_index.exist?
|
|
55
|
+
return {
|
|
56
|
+
registered: false,
|
|
57
|
+
main_index_exists: false,
|
|
58
|
+
suggestions: [
|
|
59
|
+
"Main controllers index file not found at: #{main_index.relative_path_from(app_root)}",
|
|
60
|
+
"You may need to manually import Observ controllers in your application"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
content = File.read(main_index)
|
|
66
|
+
registered = content.include?("observ")
|
|
67
|
+
|
|
68
|
+
if registered
|
|
69
|
+
{ registered: true, main_index_exists: true, path: main_index }
|
|
70
|
+
else
|
|
71
|
+
{
|
|
72
|
+
registered: false,
|
|
73
|
+
main_index_exists: true,
|
|
74
|
+
path: main_index,
|
|
75
|
+
suggestions: [
|
|
76
|
+
"Add to #{main_index.relative_path_from(app_root)}:",
|
|
77
|
+
" import './observ'"
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Generate import statement for main controllers index
|
|
84
|
+
# @param relative_path [String] Relative path to observ controllers
|
|
85
|
+
# @return [String] Import statement
|
|
86
|
+
def generate_import_statement(relative_path = "./observ")
|
|
87
|
+
"\nimport '#{relative_path}'\n"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# Generate the content for the controllers index file
|
|
93
|
+
# @param controller_files [Array<String>] List of controller file paths
|
|
94
|
+
# @param controllers_path [Pathname] Base path for controllers
|
|
95
|
+
# @return [String] Generated content
|
|
96
|
+
def generate_index_content(controller_files, controllers_path)
|
|
97
|
+
imports = controller_files.map do |file|
|
|
98
|
+
basename = File.basename(file)
|
|
99
|
+
controller_name = basename.sub("_controller.js", "").tr("_", "-")
|
|
100
|
+
class_name = basename.sub(".js", "")
|
|
101
|
+
.split("_")
|
|
102
|
+
.map(&:capitalize)
|
|
103
|
+
.join
|
|
104
|
+
.sub("Controller", "Controller")
|
|
105
|
+
|
|
106
|
+
"import #{class_name} from \"./#{basename}\""
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
registrations = controller_files.map do |file|
|
|
110
|
+
basename = File.basename(file)
|
|
111
|
+
controller_name = basename.sub("_controller.js", "").tr("_", "-")
|
|
112
|
+
class_name = basename.sub(".js", "")
|
|
113
|
+
.split("_")
|
|
114
|
+
.map(&:capitalize)
|
|
115
|
+
.join
|
|
116
|
+
.sub("Controller", "Controller")
|
|
117
|
+
|
|
118
|
+
"application.register(\"observ--#{controller_name}\", #{class_name})"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
<<~JAVASCRIPT
|
|
122
|
+
// Auto-generated index file for Observ Stimulus controllers
|
|
123
|
+
// Register all Observ controllers with the observ-- prefix
|
|
124
|
+
import { application } from "../application"
|
|
125
|
+
|
|
126
|
+
#{imports.join("\n")}
|
|
127
|
+
|
|
128
|
+
#{registrations.join("\n")}
|
|
129
|
+
JAVASCRIPT
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Log a message
|
|
133
|
+
# @param message [String] Message to log
|
|
134
|
+
def log(message)
|
|
135
|
+
if logger.respond_to?(:puts)
|
|
136
|
+
logger.puts(message)
|
|
137
|
+
else
|
|
138
|
+
logger.info(message)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
data/lib/observ.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require "observ/version"
|
|
2
|
+
require "observ/engine"
|
|
3
|
+
require "observ/configuration"
|
|
4
|
+
require "observ/asset_syncer"
|
|
5
|
+
require "observ/asset_installer"
|
|
6
|
+
require "observ/index_file_generator"
|
|
7
|
+
require "kaminari"
|
|
8
|
+
require "aasm"
|
|
9
|
+
require "ruby_llm"
|
|
10
|
+
require "ruby_llm/schema"
|
|
11
|
+
require "csv"
|
|
12
|
+
|
|
13
|
+
module Observ
|
|
14
|
+
class << self
|
|
15
|
+
attr_accessor :configuration
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.configure
|
|
19
|
+
self.configuration ||= Configuration.new
|
|
20
|
+
yield(configuration)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.config
|
|
24
|
+
self.configuration ||= Configuration.new
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# RubyLLM integration
|
|
29
|
+
require "observ/instrumenter/ruby_llm"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
namespace :observ do
|
|
2
|
+
desc "Sync Observ engine assets (CSS and JS) to main app
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
rails observ:sync_assets # Use default destinations
|
|
6
|
+
rails observ:sync_assets[custom/path] # Custom destination for styles
|
|
7
|
+
rails observ:sync_assets[styles,js] # Custom destinations for both
|
|
8
|
+
|
|
9
|
+
Examples:
|
|
10
|
+
rails observ:sync_assets
|
|
11
|
+
rails observ:sync_assets[app/javascript/stylesheets/observ]
|
|
12
|
+
rails observ:sync_assets[app/assets/stylesheets/observ,app/javascript/controllers/observ]
|
|
13
|
+
"
|
|
14
|
+
task :sync_assets, [ :styles_dest, :js_dest ] => :environment do |t, args|
|
|
15
|
+
require "observ/asset_installer"
|
|
16
|
+
|
|
17
|
+
# Get the observ gem root (this task is in observ/lib/tasks)
|
|
18
|
+
# The gem is mounted as an engine within the Rails app
|
|
19
|
+
observ_gem_root = Observ::Engine.root
|
|
20
|
+
|
|
21
|
+
# Main Rails app root
|
|
22
|
+
app_root = Rails.root
|
|
23
|
+
|
|
24
|
+
# Use provided paths or let AssetInstaller use defaults
|
|
25
|
+
installer = Observ::AssetInstaller.new(
|
|
26
|
+
gem_root: observ_gem_root,
|
|
27
|
+
app_root: app_root,
|
|
28
|
+
logger: $stdout
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
installer.sync(
|
|
32
|
+
styles_dest: args[:styles_dest],
|
|
33
|
+
js_dest: args[:js_dest]
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "Install Observ assets for the first time (includes index file generation)
|
|
38
|
+
|
|
39
|
+
Usage:
|
|
40
|
+
rails observ:install_assets # Use default destinations
|
|
41
|
+
rails observ:install_assets[custom/path] # Custom destination for styles
|
|
42
|
+
rails observ:install_assets[styles,js] # Custom destinations for both
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
rails observ:install_assets
|
|
46
|
+
rails observ:install_assets[app/javascript/stylesheets/observ]
|
|
47
|
+
rails observ:install_assets[app/assets/stylesheets/observ,app/javascript/controllers/observ]
|
|
48
|
+
"
|
|
49
|
+
task :install_assets, [ :styles_dest, :js_dest ] => :environment do |t, args|
|
|
50
|
+
require "observ/asset_installer"
|
|
51
|
+
|
|
52
|
+
# Get the observ gem root
|
|
53
|
+
observ_gem_root = Observ::Engine.root
|
|
54
|
+
|
|
55
|
+
# Main Rails app root
|
|
56
|
+
app_root = Rails.root
|
|
57
|
+
|
|
58
|
+
# Use provided paths or let AssetInstaller use defaults
|
|
59
|
+
installer = Observ::AssetInstaller.new(
|
|
60
|
+
gem_root: observ_gem_root,
|
|
61
|
+
app_root: app_root,
|
|
62
|
+
logger: $stdout
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
installer.install(
|
|
66
|
+
styles_dest: args[:styles_dest],
|
|
67
|
+
js_dest: args[:js_dest],
|
|
68
|
+
generate_index: true
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Provide shorthand aliases
|
|
74
|
+
task "observ:sync" => "observ:sync_assets"
|
|
75
|
+
task "observ:install" => "observ:install_assets"
|