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.
Files changed (209) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +778 -0
  3. data/Rakefile +49 -0
  4. data/app/assets/javascripts/observ/application.js +12 -0
  5. data/app/assets/javascripts/observ/controllers/autoscroll_controller.js +33 -0
  6. data/app/assets/javascripts/observ/controllers/chat_form_controller.js +93 -0
  7. data/app/assets/javascripts/observ/controllers/copy_controller.js +43 -0
  8. data/app/assets/javascripts/observ/controllers/dashboard_controller.js +58 -0
  9. data/app/assets/javascripts/observ/controllers/drawer_controller.js +58 -0
  10. data/app/assets/javascripts/observ/controllers/expandable_controller.js +33 -0
  11. data/app/assets/javascripts/observ/controllers/filter_controller.js +36 -0
  12. data/app/assets/javascripts/observ/controllers/index.js +52 -0
  13. data/app/assets/javascripts/observ/controllers/json_viewer_controller.js +260 -0
  14. data/app/assets/javascripts/observ/controllers/message_form_controller.js +58 -0
  15. data/app/assets/javascripts/observ/controllers/prompt_variables_controller.js +64 -0
  16. data/app/assets/javascripts/observ/controllers/text_select_controller.js +14 -0
  17. data/app/assets/stylesheets/observ/_annotations.scss +127 -0
  18. data/app/assets/stylesheets/observ/_card.scss +52 -0
  19. data/app/assets/stylesheets/observ/_chat.scss +156 -0
  20. data/app/assets/stylesheets/observ/_components.scss +460 -0
  21. data/app/assets/stylesheets/observ/_dashboard.scss +40 -0
  22. data/app/assets/stylesheets/observ/_datasets.scss +697 -0
  23. data/app/assets/stylesheets/observ/_drawer.scss +273 -0
  24. data/app/assets/stylesheets/observ/_json_viewer.scss +120 -0
  25. data/app/assets/stylesheets/observ/_layout.scss +256 -0
  26. data/app/assets/stylesheets/observ/_metrics.scss +99 -0
  27. data/app/assets/stylesheets/observ/_observations.scss +160 -0
  28. data/app/assets/stylesheets/observ/_pagination.scss +143 -0
  29. data/app/assets/stylesheets/observ/_prompts.scss +365 -0
  30. data/app/assets/stylesheets/observ/_table.scss +53 -0
  31. data/app/assets/stylesheets/observ/_variables.scss +53 -0
  32. data/app/assets/stylesheets/observ/application.scss +15 -0
  33. data/app/controllers/observ/annotations_controller.rb +144 -0
  34. data/app/controllers/observ/application_controller.rb +8 -0
  35. data/app/controllers/observ/chats_controller.rb +58 -0
  36. data/app/controllers/observ/dashboard_controller.rb +159 -0
  37. data/app/controllers/observ/dataset_items_controller.rb +85 -0
  38. data/app/controllers/observ/dataset_run_items_controller.rb +84 -0
  39. data/app/controllers/observ/dataset_runs_controller.rb +110 -0
  40. data/app/controllers/observ/datasets_controller.rb +74 -0
  41. data/app/controllers/observ/messages_controller.rb +26 -0
  42. data/app/controllers/observ/observations_controller.rb +59 -0
  43. data/app/controllers/observ/prompt_versions_controller.rb +148 -0
  44. data/app/controllers/observ/prompts_controller.rb +205 -0
  45. data/app/controllers/observ/sessions_controller.rb +45 -0
  46. data/app/controllers/observ/traces_controller.rb +86 -0
  47. data/app/forms/observ/prompt_form.rb +96 -0
  48. data/app/helpers/observ/application_helper.rb +9 -0
  49. data/app/helpers/observ/chats_helper.rb +47 -0
  50. data/app/helpers/observ/dashboard_helper.rb +154 -0
  51. data/app/helpers/observ/datasets_helper.rb +62 -0
  52. data/app/helpers/observ/pagination_helper.rb +38 -0
  53. data/app/jobs/observ/application_job.rb +4 -0
  54. data/app/jobs/observ/dataset_runner_job.rb +49 -0
  55. data/app/mailers/observ/application_mailer.rb +6 -0
  56. data/app/models/concerns/observ/agent_phaseable.rb +124 -0
  57. data/app/models/concerns/observ/agent_selectable.rb +50 -0
  58. data/app/models/concerns/observ/chat_enhancements.rb +109 -0
  59. data/app/models/concerns/observ/message_enhancements.rb +31 -0
  60. data/app/models/concerns/observ/observability_instrumentation.rb +124 -0
  61. data/app/models/concerns/observ/prompt_management.rb +320 -0
  62. data/app/models/concerns/observ/trace_association.rb +9 -0
  63. data/app/models/observ/annotation.rb +23 -0
  64. data/app/models/observ/application_record.rb +5 -0
  65. data/app/models/observ/dataset.rb +51 -0
  66. data/app/models/observ/dataset_item.rb +41 -0
  67. data/app/models/observ/dataset_run.rb +104 -0
  68. data/app/models/observ/dataset_run_item.rb +111 -0
  69. data/app/models/observ/generation.rb +56 -0
  70. data/app/models/observ/null_prompt.rb +59 -0
  71. data/app/models/observ/observation.rb +38 -0
  72. data/app/models/observ/prompt.rb +315 -0
  73. data/app/models/observ/score.rb +51 -0
  74. data/app/models/observ/session.rb +131 -0
  75. data/app/models/observ/span.rb +13 -0
  76. data/app/models/observ/trace.rb +135 -0
  77. data/app/presenters/observ/agent_select_presenter.rb +59 -0
  78. data/app/services/observ/agent_executor_service.rb +174 -0
  79. data/app/services/observ/agent_provider.rb +60 -0
  80. data/app/services/observ/agent_selection_service.rb +53 -0
  81. data/app/services/observ/chat_instrumenter.rb +523 -0
  82. data/app/services/observ/dataset_runner_service.rb +153 -0
  83. data/app/services/observ/evaluator_runner_service.rb +58 -0
  84. data/app/services/observ/evaluators/base_evaluator.rb +51 -0
  85. data/app/services/observ/evaluators/contains_evaluator.rb +53 -0
  86. data/app/services/observ/evaluators/exact_match_evaluator.rb +23 -0
  87. data/app/services/observ/evaluators/json_structure_evaluator.rb +44 -0
  88. data/app/services/observ/prompt_manager/cache_statistics.rb +82 -0
  89. data/app/services/observ/prompt_manager/caching.rb +167 -0
  90. data/app/services/observ/prompt_manager/comparison.rb +49 -0
  91. data/app/services/observ/prompt_manager/version_management.rb +96 -0
  92. data/app/services/observ/prompt_manager.rb +40 -0
  93. data/app/services/observ/trace_text_formatter.rb +349 -0
  94. data/app/validators/observ/prompt_config_validator.rb +187 -0
  95. data/app/views/kaminari/_first_page.html.erb +11 -0
  96. data/app/views/kaminari/_gap.html.erb +8 -0
  97. data/app/views/kaminari/_last_page.html.erb +11 -0
  98. data/app/views/kaminari/_next_page.html.erb +11 -0
  99. data/app/views/kaminari/_page.html.erb +12 -0
  100. data/app/views/kaminari/_paginator.html.erb +25 -0
  101. data/app/views/kaminari/_prev_page.html.erb +11 -0
  102. data/app/views/kaminari/observ/_first_page.html.erb +11 -0
  103. data/app/views/kaminari/observ/_gap.html.erb +8 -0
  104. data/app/views/kaminari/observ/_last_page.html.erb +11 -0
  105. data/app/views/kaminari/observ/_next_page.html.erb +11 -0
  106. data/app/views/kaminari/observ/_page.html.erb +12 -0
  107. data/app/views/kaminari/observ/_paginator.html.erb +25 -0
  108. data/app/views/kaminari/observ/_prev_page.html.erb +11 -0
  109. data/app/views/layouts/observ/application.html.erb +88 -0
  110. data/app/views/observ/annotations/_annotation.html.erb +13 -0
  111. data/app/views/observ/annotations/_form.html.erb +28 -0
  112. data/app/views/observ/annotations/index.html.erb +28 -0
  113. data/app/views/observ/annotations/sessions_index.html.erb +48 -0
  114. data/app/views/observ/annotations/traces_index.html.erb +48 -0
  115. data/app/views/observ/chats/_form.html.erb +45 -0
  116. data/app/views/observ/chats/index.html.erb +67 -0
  117. data/app/views/observ/chats/new.html.erb +17 -0
  118. data/app/views/observ/chats/show.html.erb +34 -0
  119. data/app/views/observ/dashboard/index.html.erb +236 -0
  120. data/app/views/observ/dataset_items/_form.html.erb +49 -0
  121. data/app/views/observ/dataset_items/edit.html.erb +18 -0
  122. data/app/views/observ/dataset_items/index.html.erb +95 -0
  123. data/app/views/observ/dataset_items/new.html.erb +18 -0
  124. data/app/views/observ/dataset_run_items/_score_close_drawer.html.erb +4 -0
  125. data/app/views/observ/dataset_run_items/_score_drawer.html.erb +75 -0
  126. data/app/views/observ/dataset_run_items/_score_success.html.erb +29 -0
  127. data/app/views/observ/dataset_run_items/_scores_cell.html.erb +19 -0
  128. data/app/views/observ/dataset_run_items/details_drawer.turbo_stream.erb +80 -0
  129. data/app/views/observ/dataset_run_items/score_drawer.turbo_stream.erb +7 -0
  130. data/app/views/observ/dataset_runs/index.html.erb +108 -0
  131. data/app/views/observ/dataset_runs/new.html.erb +57 -0
  132. data/app/views/observ/dataset_runs/review.html.erb +155 -0
  133. data/app/views/observ/dataset_runs/show.html.erb +166 -0
  134. data/app/views/observ/datasets/_form.html.erb +62 -0
  135. data/app/views/observ/datasets/_items_tab.html.erb +66 -0
  136. data/app/views/observ/datasets/_runs_tab.html.erb +82 -0
  137. data/app/views/observ/datasets/edit.html.erb +32 -0
  138. data/app/views/observ/datasets/index.html.erb +105 -0
  139. data/app/views/observ/datasets/new.html.erb +18 -0
  140. data/app/views/observ/datasets/show.html.erb +67 -0
  141. data/app/views/observ/messages/_content.html.erb +1 -0
  142. data/app/views/observ/messages/_form.html.erb +33 -0
  143. data/app/views/observ/messages/_message.html.erb +14 -0
  144. data/app/views/observ/messages/_tool_calls.html.erb +10 -0
  145. data/app/views/observ/messages/create.turbo_stream.erb +9 -0
  146. data/app/views/observ/observations/index.html.erb +97 -0
  147. data/app/views/observ/observations/show_generation.html.erb +195 -0
  148. data/app/views/observ/observations/show_span.html.erb +93 -0
  149. data/app/views/observ/prompts/_diff_content.html.erb +16 -0
  150. data/app/views/observ/prompts/_form.html.erb +111 -0
  151. data/app/views/observ/prompts/_new_form.html.erb +102 -0
  152. data/app/views/observ/prompts/_prompt_actions.html.erb +4 -0
  153. data/app/views/observ/prompts/_prompt_content_highlighted.html.erb +4 -0
  154. data/app/views/observ/prompts/_version_actions.html.erb +40 -0
  155. data/app/views/observ/prompts/compare.html.erb +155 -0
  156. data/app/views/observ/prompts/edit.html.erb +17 -0
  157. data/app/views/observ/prompts/index.html.erb +108 -0
  158. data/app/views/observ/prompts/new.html.erb +17 -0
  159. data/app/views/observ/prompts/show.html.erb +138 -0
  160. data/app/views/observ/prompts/versions.html.erb +87 -0
  161. data/app/views/observ/sessions/annotations_drawer.turbo_stream.erb +25 -0
  162. data/app/views/observ/sessions/drawer_test.turbo_stream.erb +49 -0
  163. data/app/views/observ/sessions/index.html.erb +91 -0
  164. data/app/views/observ/sessions/show.html.erb +251 -0
  165. data/app/views/observ/traces/add_to_dataset_drawer.turbo_stream.erb +48 -0
  166. data/app/views/observ/traces/annotations_drawer.turbo_stream.erb +25 -0
  167. data/app/views/observ/traces/index.html.erb +87 -0
  168. data/app/views/observ/traces/show.html.erb +285 -0
  169. data/app/views/observ/traces/text_output_drawer.turbo_stream.erb +48 -0
  170. data/app/views/shared/_drawer.html.erb +26 -0
  171. data/config/routes.rb +80 -0
  172. data/db/migrate/001_create_observ_sessions.rb +21 -0
  173. data/db/migrate/002_create_observ_traces.rb +25 -0
  174. data/db/migrate/003_create_observ_observations.rb +42 -0
  175. data/db/migrate/004_add_message_id_to_observ_traces.rb +7 -0
  176. data/db/migrate/005_create_observ_prompts.rb +21 -0
  177. data/db/migrate/006_fix_prompt_config_strings.rb +23 -0
  178. data/db/migrate/007_create_observ_annotations.rb +12 -0
  179. data/db/migrate/009_add_prompt_fields_to_observ_chats.rb +11 -0
  180. data/db/migrate/010_create_observ_datasets.rb +15 -0
  181. data/db/migrate/011_create_observ_dataset_items.rb +17 -0
  182. data/db/migrate/012_create_observ_dataset_runs.rb +22 -0
  183. data/db/migrate/013_create_observ_dataset_run_items.rb +16 -0
  184. data/db/migrate/014_create_observ_scores.rb +26 -0
  185. data/lib/generators/observ/add_phase_tracking/add_phase_tracking_generator.rb +150 -0
  186. data/lib/generators/observ/add_phase_tracking/templates/migration.rb.tt +6 -0
  187. data/lib/generators/observ/install/USAGE +27 -0
  188. data/lib/generators/observ/install/install_generator.rb +270 -0
  189. data/lib/generators/observ/install_chat/install_chat_generator.rb +313 -0
  190. data/lib/generators/observ/install_chat/templates/agents/base_agent.rb.tt +147 -0
  191. data/lib/generators/observ/install_chat/templates/agents/simple_agent.rb.tt +55 -0
  192. data/lib/generators/observ/install_chat/templates/concerns/observ_chat_enhancements.rb.tt +34 -0
  193. data/lib/generators/observ/install_chat/templates/concerns/observ_message_enhancements.rb.tt +18 -0
  194. data/lib/generators/observ/install_chat/templates/initializers/observability.rb.tt +20 -0
  195. data/lib/generators/observ/install_chat/templates/jobs/chat_response_job.rb.tt +56 -0
  196. data/lib/generators/observ/install_chat/templates/migrations/add_agent_class_name.rb.tt +6 -0
  197. data/lib/generators/observ/install_chat/templates/migrations/add_observability_session_id.rb.tt +6 -0
  198. data/lib/generators/observ/install_chat/templates/tools/think_tool.rb.tt +29 -0
  199. data/lib/generators/observ/install_chat/templates/views/messages/_content.html.erb.tt +1 -0
  200. data/lib/observ/asset_installer.rb +130 -0
  201. data/lib/observ/asset_syncer.rb +104 -0
  202. data/lib/observ/configuration.rb +108 -0
  203. data/lib/observ/engine.rb +50 -0
  204. data/lib/observ/index_file_generator.rb +142 -0
  205. data/lib/observ/instrumenter/ruby_llm.rb +6 -0
  206. data/lib/observ/version.rb +3 -0
  207. data/lib/observ.rb +29 -0
  208. data/lib/tasks/observ_tasks.rake +75 -0
  209. 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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is only loaded if RubyLLM is defined
4
+ # It provides the instrumentation hooks for RubyLLM chat tracking
5
+
6
+ # RubyLLM instrumentation will be available through ChatInstrumenter service
@@ -0,0 +1,3 @@
1
+ module Observ
2
+ VERSION = "0.5.0"
3
+ 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"