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,313 @@
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 installing Observ chat/agent testing feature
9
+ #
10
+ # This generator enhances RubyLLM infrastructure with Observ-specific
11
+ # agent capabilities and observability features.
12
+ #
13
+ # Prerequisites:
14
+ # - RubyLLM gem installed (gem 'ruby_llm')
15
+ # - rails generate ruby_llm:install (run first)
16
+ # - rails db:migrate
17
+ # - rails ruby_llm:load_models
18
+ #
19
+ # Usage:
20
+ # rails generate observ:install:chat
21
+ # rails generate observ:install:chat --skip-tools
22
+ # rails generate observ:install:chat --skip-migrations
23
+ class InstallChatGenerator < Rails::Generators::Base
24
+ include ActiveRecord::Generators::Migration
25
+
26
+ source_root File.expand_path("templates", __dir__)
27
+
28
+ class_option :skip_tools,
29
+ type: :boolean,
30
+ desc: "Skip tool class generation",
31
+ default: false
32
+
33
+ class_option :skip_migrations,
34
+ type: :boolean,
35
+ desc: "Skip migration generation",
36
+ default: false
37
+
38
+ class_option :skip_job,
39
+ type: :boolean,
40
+ desc: "Skip ChatResponseJob generation",
41
+ default: false
42
+
43
+ class_option :with_phase_tracking,
44
+ type: :boolean,
45
+ desc: "Include phase tracking for multi-phase agents",
46
+ default: false
47
+
48
+ def check_prerequisites
49
+ say "\n"
50
+ say "=" * 80, :cyan
51
+ say "Observ Chat Feature Installation", :cyan
52
+ say "=" * 80, :cyan
53
+ say "\n"
54
+
55
+ check_ruby_llm_gem
56
+ check_ruby_llm_models_installed
57
+ end
58
+
59
+ def create_migrations
60
+ return if options[:skip_migrations]
61
+
62
+ say "Creating Observ-specific migrations...", :cyan
63
+ say "-" * 80, :cyan
64
+
65
+ migration_template "migrations/add_agent_class_name.rb.tt",
66
+ "db/migrate/add_agent_class_name_to_chats.rb"
67
+
68
+ migration_template "migrations/add_observability_session_id.rb.tt",
69
+ "db/migrate/add_observability_session_id_to_chats.rb"
70
+
71
+ say " ✓ Created agent_class_name migration", :green
72
+ say " ✓ Created observability_session_id migration", :green
73
+ say "\n"
74
+ end
75
+
76
+ def enhance_models
77
+ say "Enhancing RubyLLM models with Observ functionality...", :cyan
78
+ say "-" * 80, :cyan
79
+
80
+ enhance_chat_model
81
+ enhance_message_model
82
+
83
+ say "\n"
84
+ end
85
+
86
+ def create_agent_infrastructure
87
+ say "Creating agent infrastructure...", :cyan
88
+ say "-" * 80, :cyan
89
+
90
+ template "agents/base_agent.rb.tt", "app/agents/base_agent.rb"
91
+
92
+ say " ✓ Created BaseAgent", :green
93
+ say " ℹ AgentProvider is now available as Observ::AgentProvider (no generation needed)", :yellow
94
+ say " ℹ AgentSelectable is now available as Observ::AgentSelectable (no generation needed)", :yellow
95
+ say " ℹ PromptManagement is now available as Observ::PromptManagement (no generation needed)", :yellow
96
+ say "\n"
97
+ end
98
+
99
+ def create_example_agent
100
+ say "Creating example agent...", :cyan
101
+ say "-" * 80, :cyan
102
+
103
+ template "agents/simple_agent.rb.tt", "app/agents/simple_agent.rb"
104
+
105
+ say " ✓ Created SimpleAgent", :green
106
+ say "\n"
107
+ end
108
+
109
+ def create_job
110
+ return if options[:skip_job]
111
+
112
+ say "Creating ChatResponseJob...", :cyan
113
+ say "-" * 80, :cyan
114
+
115
+ template "jobs/chat_response_job.rb.tt", "app/jobs/chat_response_job.rb"
116
+
117
+ say " ✓ Created ChatResponseJob", :green
118
+ say "\n"
119
+ end
120
+
121
+ def create_tools
122
+ return if options[:skip_tools]
123
+
124
+ say "Creating tool classes...", :cyan
125
+ say "-" * 80, :cyan
126
+
127
+ template "tools/think_tool.rb.tt", "app/tools/think_tool.rb"
128
+
129
+ say " ✓ Created ThinkTool (basic example)", :green
130
+ say " ℹ For advanced tools (web search, etc.), see documentation", :yellow
131
+ say "\n"
132
+ end
133
+
134
+ def create_view_partials
135
+ say "Creating view partials...", :cyan
136
+ say "-" * 80, :cyan
137
+
138
+ template "views/messages/_content.html.erb.tt", "app/views/messages/_content.html.erb"
139
+
140
+ say " ✓ Created messages/_content partial", :green
141
+ say "\n"
142
+ end
143
+
144
+ def create_initializer
145
+ say "Creating observability initializer...", :cyan
146
+ say "-" * 80, :cyan
147
+
148
+ template "initializers/observability.rb.tt", "config/initializers/observability.rb"
149
+
150
+ say " ✓ Created observability initializer (debug logging enabled)", :green
151
+ say "\n"
152
+ end
153
+
154
+ def add_phase_tracking
155
+ return unless options[:with_phase_tracking]
156
+
157
+ say "Adding phase tracking support...", :cyan
158
+ say "-" * 80, :cyan
159
+
160
+ # Call the add_phase_tracking generator
161
+ generate "observ:add_phase_tracking"
162
+
163
+ say "\n"
164
+ end
165
+
166
+ def show_post_install_instructions
167
+ say "\n"
168
+ say "=" * 80, :green
169
+ say "Observ Chat Feature Installation Complete!", :green
170
+ say "=" * 80, :green
171
+ say "\n"
172
+
173
+ say "Next steps:", :cyan
174
+ say "\n"
175
+
176
+ say "1. Run migrations:", :cyan
177
+ say " rails db:migrate", :white
178
+ say "\n"
179
+
180
+ say "2. Start your Rails server and visit:", :cyan
181
+ say " http://localhost:3000/observ/chats", :white
182
+ say "\n"
183
+
184
+ say "3. Create your first agent by extending BaseAgent:", :cyan
185
+ say " See app/agents/simple_agent.rb for an example", :white
186
+ say "\n"
187
+
188
+ unless options[:with_phase_tracking]
189
+ say "Optional: Add phase tracking for multi-phase agents:", :cyan
190
+ say " rails generate observ:add_phase_tracking", :white
191
+ say "\n"
192
+ end
193
+
194
+ say "Documentation:", :cyan
195
+ say " • Agent development: observ/docs/AGENT_DEVELOPMENT.md", :white
196
+ say " • Tool development: observ/docs/TOOL_DEVELOPMENT.md", :white
197
+ say "\n"
198
+ end
199
+
200
+ private
201
+
202
+ def check_ruby_llm_gem
203
+ unless gem_installed?("ruby_llm")
204
+ raise Thor::Error, <<~ERROR
205
+ RubyLLM gem not found!
206
+
207
+ This generator requires RubyLLM to be installed first.
208
+
209
+ Please run:
210
+ 1. Add to Gemfile: gem 'ruby_llm'
211
+ 2. bundle install
212
+ 3. rails generate ruby_llm:install
213
+ 4. rails db:migrate
214
+ 5. rails ruby_llm:load_models
215
+ #{' '}
216
+ Then run this generator again.
217
+ ERROR
218
+ end
219
+ say " ✓ RubyLLM gem found", :green
220
+ end
221
+
222
+ def check_ruby_llm_models_installed
223
+ models_to_check = %w[Chat Message ToolCall Model]
224
+ missing_models = []
225
+
226
+ models_to_check.each do |model_name|
227
+ model_path = Rails.root.join("app/models/#{model_name.underscore}.rb")
228
+ unless File.exist?(model_path)
229
+ missing_models << model_name
230
+ end
231
+ end
232
+
233
+ if missing_models.any?
234
+ raise Thor::Error, <<~ERROR
235
+ RubyLLM models not found: #{missing_models.join(', ')}
236
+
237
+ This generator requires ruby_llm:install to be run first.
238
+
239
+ Please run:
240
+ 1. rails generate ruby_llm:install
241
+ 2. rails db:migrate
242
+ 3. rails ruby_llm:load_models
243
+ #{' '}
244
+ Then run this generator again.
245
+ ERROR
246
+ end
247
+
248
+ say " ✓ RubyLLM models found (Chat, Message, ToolCall, Model)", :green
249
+ end
250
+
251
+ def enhance_chat_model
252
+ # Include the concern from gem in Chat model if not already included
253
+ chat_content = File.read(Rails.root.join("app/models/chat.rb"))
254
+
255
+ unless chat_content.include?("Observ::ChatEnhancements")
256
+ inject_into_file "app/models/chat.rb", after: /class Chat < ApplicationRecord\n/ do
257
+ " include Observ::ChatEnhancements\n\n"
258
+ end
259
+ say " ✓ Included Observ::ChatEnhancements in Chat model", :green
260
+ else
261
+ say " ⚠ Chat model already includes Observ::ChatEnhancements", :yellow
262
+ end
263
+
264
+ # Add agent_class method if agent_class_name column exists
265
+ unless chat_content.include?("def agent_class")
266
+ inject_into_file "app/models/chat.rb", before: /^end\s*$/ do
267
+ <<~RUBY
268
+
269
+ # Return the agent class for this chat
270
+ # Override this method if you need custom agent class resolution
271
+ def agent_class
272
+ return BaseAgent if agent_class_name.blank?
273
+
274
+ agent_class_name.constantize
275
+ rescue NameError
276
+ Rails.logger.warn "Agent class \#{agent_class_name} not found, using BaseAgent"
277
+ BaseAgent
278
+ end
279
+ RUBY
280
+ end
281
+ say " ✓ Added agent_class method to Chat model", :green
282
+ else
283
+ say " ⚠ Chat model already has agent_class method", :yellow
284
+ end
285
+ end
286
+
287
+ def enhance_message_model
288
+ # Include the concern from gem in Message model if not already included
289
+ message_content = File.read(Rails.root.join("app/models/message.rb"))
290
+
291
+ unless message_content.include?("Observ::MessageEnhancements")
292
+ inject_into_file "app/models/message.rb", after: /class Message < ApplicationRecord\n/ do
293
+ " include Observ::MessageEnhancements\n\n"
294
+ end
295
+ say " ✓ Included Observ::MessageEnhancements in Message model", :green
296
+ else
297
+ say " ⚠ Message model already includes Observ::MessageEnhancements", :yellow
298
+ end
299
+ end
300
+
301
+ def gem_installed?(gem_name)
302
+ Gem::Specification.find_all_by_name(gem_name).any?
303
+ rescue Gem::LoadError
304
+ false
305
+ end
306
+
307
+ # Helper for migration timestamps
308
+ def migration_version
309
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,147 @@
1
+ # Base interface for all agents
2
+ # Defines the contract that all agents must implement
3
+ #
4
+ # Responsibilities:
5
+ # - Define required interface methods (system_prompt, default_model)
6
+ # - Define optional interface methods (tools, initial_greeting)
7
+ # - Provide setup methods that work with the interface
8
+ #
9
+ # Usage:
10
+ # class MyAgent < BaseAgent
11
+ # def self.system_prompt
12
+ # "You are a helpful assistant."
13
+ # end
14
+ #
15
+ # def self.default_model
16
+ # "gpt-4o-mini"
17
+ # end
18
+ #
19
+ # def self.tools
20
+ # [MyTool]
21
+ # end
22
+ # end
23
+ #
24
+ # For agents that want prompt management, include the Observ::PromptManagement concern:
25
+ # class MyAgent < BaseAgent
26
+ # include Observ::PromptManagement
27
+ # # ...
28
+ # end
29
+ class BaseAgent
30
+ # ============================================
31
+ # INTERFACE METHODS - Override in subclasses
32
+ # ============================================
33
+
34
+ # System prompt for the agent
35
+ # @return [String] The system prompt
36
+ def self.system_prompt
37
+ raise NotImplementedError, "#{name} must implement .system_prompt"
38
+ end
39
+
40
+ # Default model for this agent
41
+ # @return [String] The default model identifier
42
+ def self.default_model
43
+ raise NotImplementedError, "#{name} must implement .default_model"
44
+ end
45
+
46
+ # Tools available to this agent
47
+ # @return [Array<Class>] Array of tool classes
48
+ def self.tools
49
+ []
50
+ end
51
+
52
+ # Initial greeting message when chat starts
53
+ # @return [String, nil] The greeting message or nil
54
+ def self.initial_greeting
55
+ nil
56
+ end
57
+
58
+ # Model to use for this agent
59
+ # Can be overridden by concerns (e.g., Observ::PromptManagement)
60
+ # @return [String] The model identifier to use
61
+ def self.model
62
+ default_model
63
+ end
64
+
65
+ # Default model parameters for this agent (temperature, max_tokens, etc.)
66
+ # @return [Hash] Hash of model parameters
67
+ def self.default_model_parameters
68
+ {} # Override in subclasses for custom defaults
69
+ end
70
+
71
+ # Model parameters to use for this agent
72
+ # Can be overridden by concerns (e.g., Observ::PromptManagement)
73
+ # @return [Hash] The model parameters to use
74
+ def self.model_parameters
75
+ default_model_parameters
76
+ end
77
+
78
+ # ============================================
79
+ # SETUP METHODS - Work with the interface
80
+ # ============================================
81
+
82
+ # Setup instructions for the chat session
83
+ # @param chat [Chat] The chat session
84
+ # @return [Chat] The configured chat session
85
+ def self.setup_instructions(chat)
86
+ chat.with_instructions(system_prompt) if system_prompt.present?
87
+ chat
88
+ end
89
+
90
+ # Setup tools for the chat session
91
+ # @param chat [Chat] The chat session
92
+ # @return [Chat] The configured chat session
93
+ def self.setup_tools(chat)
94
+ if tools.any?
95
+ instantiated_tools = tools.map do |tool|
96
+ if tool.instance_of?(Class)
97
+ observ_session = chat.observ_session if chat.respond_to?(:observ_session)
98
+ tool.new(observ_session)
99
+ else
100
+ if tool.respond_to?(:observability=) && chat.respond_to?(:observ_session)
101
+ tool.observability = chat.observ_session
102
+ end
103
+ tool
104
+ end
105
+ end
106
+ chat.with_tools(*instantiated_tools)
107
+ end
108
+ chat
109
+ end
110
+
111
+ # Setup model parameters for the chat session
112
+ # @param chat [Chat] The chat session
113
+ # @return [Chat] The configured chat session
114
+ def self.setup_parameters(chat)
115
+ params = model_parameters
116
+
117
+ # Convert string numeric values to proper types for API compatibility
118
+ # This is necessary because prompt configs may return string values
119
+ normalized_params = params.transform_values do |value|
120
+ case value
121
+ when String
122
+ # Convert numeric strings to numbers
123
+ if value.match?(/\A-?\d+\.?\d*\z/)
124
+ value.include?('.') ? value.to_f : value.to_i
125
+ else
126
+ value
127
+ end
128
+ else
129
+ value
130
+ end
131
+ end
132
+
133
+ chat.with_params(**normalized_params) if normalized_params.any?
134
+ chat
135
+ end
136
+
137
+ # Send initial greeting message
138
+ # @param chat [Chat] The chat session
139
+ def self.send_initial_greeting(chat)
140
+ return unless initial_greeting
141
+
142
+ chat.messages.create!(
143
+ role: :assistant,
144
+ content: initial_greeting
145
+ )
146
+ end
147
+ end
@@ -0,0 +1,55 @@
1
+ # SimpleAgent - A basic agent example
2
+ #
3
+ # This agent demonstrates the minimum required implementation
4
+ # for a working agent in the Observ chat UI.
5
+ #
6
+ # To create your own agent:
7
+ # 1. Extend BaseAgent
8
+ # 2. Include Observ::AgentSelectable to make it appear in the UI
9
+ # 3. Implement required methods: system_prompt, default_model, display_name
10
+ # 4. Optionally add tools and initial_greeting
11
+ class SimpleAgent < BaseAgent
12
+ include Observ::AgentSelectable
13
+
14
+ # Display name shown in the Observ UI
15
+ def self.display_name
16
+ "Simple Agent"
17
+ end
18
+
19
+ # Description shown in agent selection
20
+ def self.description
21
+ "A basic conversational agent"
22
+ end
23
+
24
+ # System prompt that defines the agent's behavior
25
+ def self.system_prompt
26
+ <<~PROMPT
27
+ You are a helpful AI assistant.
28
+
29
+ Your role is to:
30
+ - Answer questions clearly and concisely
31
+ - Be friendly and professional
32
+ - Admit when you don't know something
33
+
34
+ Current date: #{Time.current.strftime("%B %d, %Y")}
35
+ PROMPT
36
+ end
37
+
38
+ # Default model to use for this agent
39
+ def self.default_model
40
+ "gpt-4o-mini"
41
+ end
42
+
43
+ # Optional: Greeting message when chat starts
44
+ def self.initial_greeting
45
+ <<~GREETING
46
+ Hello! I'm a simple AI assistant. How can I help you today?
47
+ GREETING
48
+ end
49
+
50
+ # Optional: Tools available to this agent
51
+ # Uncomment and add tool classes as needed:
52
+ # def self.tools
53
+ # [ThinkTool]
54
+ # end
55
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObservChatEnhancements
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include Observ::ObservabilityInstrumentation
8
+
9
+ # Consolidated callback for agent initialization to reduce redundant queries
10
+ after_create :initialize_agent, if: -> { agent_class_name.present? }
11
+ end
12
+
13
+ def agent_class
14
+ return BaseAgent if agent_class_name.blank?
15
+
16
+ agent_class_name.constantize
17
+ rescue NameError
18
+ Rails.logger.warn "Agent class #{agent_class_name} not found, using BaseAgent"
19
+ BaseAgent
20
+ end
21
+
22
+ def setup_tools
23
+ agent_class.setup_tools(self)
24
+ end
25
+
26
+ private
27
+
28
+ def initialize_agent
29
+ # Execute both agent setup steps in one consolidated callback
30
+ # This prevents redundant association loading between callbacks
31
+ agent_class.setup_instructions(self)
32
+ agent_class.send_initial_greeting(self)
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObservMessageEnhancements
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include Observ::TraceAssociation
8
+
9
+ broadcasts_to ->(message) { "chat_#{message.chat_id}" }, partial: "observ/messages/message"
10
+ end
11
+
12
+ def broadcast_append_chunk(content)
13
+ broadcast_append_to "chat_#{chat_id}",
14
+ target: "message_#{id}_content",
15
+ partial: "observ/messages/content",
16
+ locals: { content: content }
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Observability configuration
4
+ # This file is optional and is used to configure observability settings
5
+
6
+ Rails.application.configure do
7
+ config.observability = ActiveSupport::OrderedOptions.new
8
+
9
+ # Enable observability instrumentation
10
+ # When enabled, sessions, traces, and observations are automatically tracked
11
+ config.observability.enabled = true
12
+
13
+ # Automatically instrument RubyLLM chats with observability
14
+ # When enabled, LLM calls, tool usage, and metrics are tracked
15
+ config.observability.auto_instrument_chats = true
16
+
17
+ # Enable debug logging for observability metrics
18
+ # When enabled, job completion metrics (tokens, cost) will be logged
19
+ config.observability.debug = Rails.env.development?
20
+ end
@@ -0,0 +1,56 @@
1
+ class ChatResponseJob < ApplicationJob
2
+ retry_on RubyLLM::BadRequestError, wait: 2.seconds, attempts: 1
3
+
4
+ def perform(chat_id, content)
5
+ chat = Chat.find(chat_id)
6
+
7
+ # Observability is automatically enabled via after_find callback
8
+ # All LLM calls, tool calls, and metrics are tracked automatically
9
+
10
+ chat.setup_tools
11
+
12
+ begin
13
+ # Model parameters (temperature, max_tokens, etc.) are automatically configured
14
+ # via the initialize_agent callback when the chat is created
15
+ chat.ask(content) do |chunk|
16
+ if chunk.content && !chunk.content.blank?
17
+ message = chat.messages.last
18
+ message.broadcast_append_chunk(chunk.content)
19
+ end
20
+ end
21
+ rescue RubyLLM::BadRequestError => e
22
+ Rails.logger.error "[ChatResponseJob] BadRequestError: #{e.message}"
23
+
24
+ error_message = chat.messages.create!(
25
+ role: :assistant,
26
+ content: "I apologize, but I encountered an error while processing your request. This might be due to a tool call issue. Please try rephrasing your question or try again."
27
+ )
28
+
29
+ error_message.broadcast_replace_to(
30
+ "chat_#{chat.id}",
31
+ target: "messages",
32
+ partial: "observ/messages/message",
33
+ locals: { message: error_message }
34
+ )
35
+
36
+ raise
37
+ end
38
+
39
+ chat.finalize_observability_session if chat.observ_session
40
+
41
+ if observability_debug_enabled? && chat.observ_session
42
+ metrics = chat.observ_session.session_metrics
43
+ Rails.logger.info "[Observability] Job completed - Tokens: #{metrics[:total_tokens]}, Cost: $#{metrics[:total_cost]}"
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def observability_debug_enabled?
50
+ Rails.configuration.respond_to?(:observability) &&
51
+ Rails.configuration.observability.respond_to?(:debug) &&
52
+ Rails.configuration.observability.debug
53
+ rescue NoMethodError
54
+ false
55
+ end
56
+ end
@@ -0,0 +1,6 @@
1
+ class AddAgentClassNameToChats < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ add_column :chats, :agent_class_name, :string
4
+ add_index :chats, :agent_class_name
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class AddObservabilitySessionIdToChats < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ add_column :chats, :observability_session_id, :string
4
+ add_index :chats, :observability_session_id
5
+ end
6
+ end
@@ -0,0 +1,29 @@
1
+ require "ruby_llm"
2
+
3
+ # ThinkTool - A basic tool for agent reflection
4
+ #
5
+ # This tool allows agents to "think out loud" and reflect on their
6
+ # reasoning process. Useful for debugging and understanding agent behavior.
7
+ #
8
+ # To create your own tools:
9
+ # 1. Extend RubyLLM::Tool
10
+ # 2. Add description and params
11
+ # 3. Implement execute method
12
+ # 4. Add tool to agent's tools array
13
+ class ThinkTool < RubyLLM::Tool
14
+ description "Reflect on current progress and plan next steps. Use this to organize your thoughts."
15
+
16
+ param :reflection,
17
+ desc: "Your thoughts on what you've learned so far and what to do next",
18
+ type: :string
19
+
20
+ attr_accessor :observability
21
+
22
+ def initialize(observability = nil)
23
+ @observability = observability
24
+ end
25
+
26
+ def execute(reflection:)
27
+ "Reflection noted: #{reflection}"
28
+ end
29
+ end