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,154 @@
1
+ module Observ
2
+ module DashboardHelper
3
+ def format_currency(amount)
4
+ return "$0.00" if amount.nil? || amount.zero?
5
+ "$#{sprintf('%.4f', amount)}"
6
+ end
7
+
8
+ def format_duration_ms(ms)
9
+ return "0ms" if ms.nil? || ms.zero?
10
+ return "#{ms.round(0)}ms" if ms < 1000
11
+ "#{(ms / 1000.0).round(1)}s"
12
+ end
13
+
14
+ def format_duration_s(seconds)
15
+ return "0s" if seconds.nil? || seconds.zero?
16
+ return "#{seconds.round(1)}s" if seconds < 60
17
+ minutes = (seconds / 60).floor
18
+ remaining_seconds = (seconds % 60).round(0)
19
+ "#{minutes}m #{remaining_seconds}s"
20
+ end
21
+
22
+ def format_tokens(count)
23
+ return "0" if count.nil? || count.zero?
24
+ return count.to_s if count < 1000
25
+ return "#{(count / 1000.0).round(1)}K" if count < 1_000_000
26
+ "#{(count / 1_000_000.0).round(1)}M"
27
+ end
28
+
29
+ def format_number(number)
30
+ return "0" if number.nil? || number.zero?
31
+ number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
32
+ end
33
+
34
+ def observ_status_badge(status)
35
+ css_class = case status&.downcase
36
+ when "completed", "success"
37
+ "observ-badge--success"
38
+ when "active", "running", "in_progress"
39
+ "observ-badge--info"
40
+ when "failed", "error"
41
+ "observ-badge--danger"
42
+ else
43
+ "observ-badge--warning"
44
+ end
45
+
46
+ content_tag(:span, status&.titleize || "Unknown", class: "observ-badge #{css_class}")
47
+ end
48
+
49
+ def observ_trend_badge(percentage)
50
+ return content_tag(:span, "0%", class: "observ-trend observ-trend--neutral") if percentage.zero?
51
+
52
+ css_class = percentage.positive? ? "observ-trend--positive" : "observ-trend--negative"
53
+ icon = percentage.positive? ? "↑" : "↓"
54
+
55
+ content_tag(:span, class: "observ-trend #{css_class}") do
56
+ "#{icon} #{percentage.abs}%"
57
+ end
58
+ end
59
+
60
+ def truncate_id(id, length = 8)
61
+ return "" if id.nil?
62
+ id.to_s[0...length]
63
+ end
64
+
65
+ def observ_percentage(value)
66
+ "#{value.round(1)}%"
67
+ end
68
+
69
+ def observ_timestamp(time)
70
+ return "N/A" if time.nil?
71
+ time.strftime("%Y-%m-%d %H:%M:%S")
72
+ end
73
+
74
+ def observ_relative_time(time)
75
+ return "N/A" if time.nil?
76
+ distance_of_time_in_words_to_now(time) + " ago"
77
+ end
78
+
79
+ def observ_session_status(session)
80
+ session.end_time.present? ? "completed" : "active"
81
+ end
82
+
83
+ def observ_trace_status(trace)
84
+ trace.end_time.present? ? "completed" : "active"
85
+ end
86
+
87
+ def observ_model_badge(model)
88
+ return content_tag(:span, "Unknown", class: "observ-model-badge") if model.blank?
89
+
90
+ content_tag(:span, model, class: "observ-model-badge")
91
+ end
92
+
93
+ def observ_json_preview(json_data, max_length = 100)
94
+ return "" if json_data.nil?
95
+
96
+ text = json_data.is_a?(String) ? json_data : json_data.to_json
97
+ truncate(text, length: max_length)
98
+ end
99
+
100
+ def format_json_with_newlines(data)
101
+ return "" if data.nil?
102
+
103
+ # Convert to JSON with pretty formatting
104
+ json_string = JSON.pretty_generate(data)
105
+
106
+ # This regex finds string values in JSON and unescapes newlines within them
107
+ # It preserves the JSON structure while making newlines visible
108
+ json_string.gsub(/: "((?:[^"\\]|\\.)*)"/m) do |match|
109
+ content = $1
110
+ # Unescape the newlines in the string content
111
+ unescaped = content.gsub('\\n', "\n")
112
+ .gsub('\\t', "\t")
113
+ .gsub('\\r', "\r")
114
+ ': "' + unescaped + '"'
115
+ end
116
+ end
117
+
118
+ def render_json_viewer(data, compact: false)
119
+ return "" if data.nil?
120
+
121
+ css_classes = [ "observ-json-viewer" ]
122
+ css_classes << "observ-json-viewer--compact" if compact
123
+
124
+ content_tag(:div,
125
+ "",
126
+ class: css_classes.join(" "),
127
+ data: {
128
+ controller: "observ--json-viewer",
129
+ observ__json_viewer_data_value: data.to_json
130
+ }
131
+ )
132
+ end
133
+
134
+ def render_input_output(content, compact: false)
135
+ return "" if content.nil?
136
+
137
+ # Try to parse as JSON
138
+ begin
139
+ parsed_data = JSON.parse(content)
140
+ # If it's a Hash or Array, use the JSON viewer
141
+ if parsed_data.is_a?(Hash) || parsed_data.is_a?(Array)
142
+ return render_json_viewer(parsed_data, compact: compact)
143
+ end
144
+ rescue JSON::ParserError
145
+ # Not valid JSON, fall through to plain text display
146
+ end
147
+
148
+ # Display as plain text
149
+ css_classes = [ "observ-code-block" ]
150
+ css_classes << "observ-code-block--compact" if compact
151
+ content_tag(:pre, content, class: css_classes.join(" "))
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Observ
4
+ module DatasetsHelper
5
+ # Returns the appropriate badge class for a run status
6
+ def run_status_badge_class(status)
7
+ case status.to_s
8
+ when "pending"
9
+ "observ-badge--default"
10
+ when "running"
11
+ "observ-badge--info"
12
+ when "completed"
13
+ "observ-badge--success"
14
+ when "failed"
15
+ "observ-badge--danger"
16
+ else
17
+ "observ-badge--default"
18
+ end
19
+ end
20
+
21
+ # Returns the appropriate badge class for a run item status
22
+ def run_item_status_badge_class(status)
23
+ case status.to_s
24
+ when "pending"
25
+ "observ-badge--default"
26
+ when "succeeded"
27
+ "observ-badge--success"
28
+ when "failed"
29
+ "observ-badge--danger"
30
+ else
31
+ "observ-badge--default"
32
+ end
33
+ end
34
+
35
+ # Formats JSON for display
36
+ def format_json_preview(data, max_length: 100)
37
+ return nil if data.blank?
38
+ text = data.is_a?(Hash) ? data.to_json : data.to_s
39
+ text.length > max_length ? "#{text[0...max_length]}..." : text
40
+ end
41
+
42
+ # Formats trace data for display in forms/previews
43
+ # Handles both JSON and string data
44
+ def format_trace_data(data)
45
+ return "" if data.blank?
46
+
47
+ if data.is_a?(Hash) || data.is_a?(Array)
48
+ JSON.pretty_generate(data)
49
+ elsif data.is_a?(String)
50
+ # Try to parse and pretty-print if it's JSON
51
+ begin
52
+ parsed = JSON.parse(data)
53
+ JSON.pretty_generate(parsed)
54
+ rescue JSON::ParserError
55
+ data
56
+ end
57
+ else
58
+ data.to_s
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Observ
4
+ module PaginationHelper
5
+ # Renders pagination controls with info
6
+ # Usage: <%= observ_pagination(@collection) %>
7
+ def observ_pagination(collection)
8
+ return unless collection.respond_to?(:current_page)
9
+
10
+ content_tag(:div, class: "observ-pagination") do
11
+ safe_join([
12
+ pagination_info(collection),
13
+ pagination_links(collection)
14
+ ])
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def pagination_info(collection)
21
+ return "" if collection.total_count.zero?
22
+
23
+ from = collection.offset_value + 1
24
+ to = [ collection.offset_value + collection.limit_value, collection.total_count ].min
25
+ total = collection.total_count
26
+
27
+ content_tag(:div, class: "observ-pagination__info") do
28
+ "Showing #{from}-#{to} of #{total}"
29
+ end
30
+ end
31
+
32
+ def pagination_links(collection)
33
+ content_tag(:div, class: "observ-pagination__links") do
34
+ paginate collection, theme: "observ"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,4 @@
1
+ module Observ
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Observ
4
+ # Background job for executing dataset evaluations asynchronously
5
+ #
6
+ # This job wraps the DatasetRunnerService to allow dataset runs
7
+ # to be processed in the background via ActiveJob.
8
+ #
9
+ # Usage:
10
+ # DatasetRunnerJob.perform_later(dataset_run.id)
11
+ #
12
+ # The job will:
13
+ # - Find the dataset run by ID
14
+ # - Skip execution if the run is already completed or running
15
+ # - Execute the DatasetRunnerService
16
+ #
17
+ class DatasetRunnerJob < ApplicationJob
18
+ queue_as :default
19
+
20
+ # Retry on transient failures with exponential backoff
21
+ retry_on StandardError, wait: :polynomially_longer, attempts: 3 do |job, error|
22
+ # Mark the run as failed if all retries exhausted
23
+ dataset_run = Observ::DatasetRun.find_by(id: job.arguments.first)
24
+ if dataset_run
25
+ dataset_run.update!(
26
+ status: :failed,
27
+ metadata: dataset_run.metadata.merge(
28
+ error: error.message,
29
+ error_class: error.class.name,
30
+ failed_at: Time.current.iso8601,
31
+ retries_exhausted: true
32
+ )
33
+ )
34
+ end
35
+ end
36
+
37
+ def perform(dataset_run_id)
38
+ dataset_run = Observ::DatasetRun.find(dataset_run_id)
39
+
40
+ # Skip if already completed or failed
41
+ return if dataset_run.finished?
42
+
43
+ # Skip if already running (avoid duplicate execution)
44
+ return if dataset_run.running?
45
+
46
+ DatasetRunnerService.new(dataset_run).call
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,6 @@
1
+ module Observ
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Observ
4
+ # AgentPhaseable adds phase tracking capabilities to Chat models
5
+ #
6
+ # This concern is optional and should only be included if your agents
7
+ # need to track multi-phase workflows (e.g., scoping -> research -> writing).
8
+ #
9
+ # Prerequisites:
10
+ # - Chat model must have a `current_phase` string column
11
+ # - Chat model must include `Observ::ObservabilityInstrumentation`
12
+ # - Run: rails generate observ:add_phase_tracking
13
+ #
14
+ # Usage:
15
+ # class Chat < ApplicationRecord
16
+ # include Observ::ObservabilityInstrumentation
17
+ # include Observ::AgentPhaseable
18
+ # end
19
+ #
20
+ # Example:
21
+ # chat = Chat.create!
22
+ # chat.transition_to_phase('research')
23
+ # chat.current_phase # => 'research'
24
+ #
25
+ module AgentPhaseable
26
+ extend ActiveSupport::Concern
27
+
28
+ included do
29
+ # Validate that the current_phase column exists
30
+ unless column_names.include?("current_phase")
31
+ raise "AgentPhaseable requires a 'current_phase' column. " \
32
+ "Run: rails generate observ:add_phase_tracking"
33
+ end
34
+
35
+ validates :current_phase,
36
+ inclusion: { in: :allowed_phases, allow_nil: true },
37
+ if: -> { respond_to?(:allowed_phases) }
38
+ end
39
+
40
+ # Override observability_metadata to include phase information
41
+ def observability_metadata
42
+ metadata = super
43
+ metadata[:agent_phase] = current_phase if current_phase.present?
44
+ metadata
45
+ end
46
+
47
+ # Override observability_context to include phase information
48
+ def observability_context
49
+ context = super
50
+ context[:phase] = current_phase if current_phase.present?
51
+ context
52
+ end
53
+
54
+ # Transition to a new phase with observability tracking
55
+ #
56
+ # @param new_phase [String, Symbol] The phase to transition to
57
+ # @param metadata [Hash] Optional additional metadata to record with the transition
58
+ # @return [Boolean] true if the transition was successful
59
+ #
60
+ # Example:
61
+ # chat.transition_to_phase('research', depth: 'deep')
62
+ #
63
+ def transition_to_phase(new_phase, **metadata)
64
+ old_phase = current_phase
65
+ new_phase = new_phase.to_s
66
+
67
+ # Validate phase if allowed_phases is defined
68
+ if respond_to?(:allowed_phases) && !allowed_phases.include?(new_phase)
69
+ raise ArgumentError, "Invalid phase: #{new_phase}. Allowed phases: #{allowed_phases.join(', ')}"
70
+ end
71
+
72
+ self.current_phase = new_phase
73
+ save!
74
+
75
+ # Update observability context if session exists
76
+ if observ_session
77
+ transition_metadata = {
78
+ phase: new_phase,
79
+ phase_transition: "#{old_phase || 'initial'} -> #{new_phase}"
80
+ }.merge(metadata)
81
+
82
+ update_observability_context(transition_metadata)
83
+ end
84
+
85
+ Rails.logger.info "[AgentPhase] #{self.class.name}##{id} transitioned: #{old_phase || 'initial'} -> #{new_phase}"
86
+
87
+ true
88
+ rescue StandardError => e
89
+ Rails.logger.error "[AgentPhase] Failed to transition to #{new_phase}: #{e.message}"
90
+ false
91
+ end
92
+
93
+ # Check if currently in a specific phase
94
+ #
95
+ # @param phase [String, Symbol] The phase to check
96
+ # @return [Boolean] true if in the specified phase
97
+ #
98
+ # Example:
99
+ # chat.in_phase?('research') # => true
100
+ #
101
+ def in_phase?(phase)
102
+ current_phase == phase.to_s
103
+ end
104
+
105
+ # Get a list of allowed phases
106
+ # Override this method in your Chat model to define valid phases
107
+ #
108
+ # Example:
109
+ # class Chat < ApplicationRecord
110
+ # include Observ::AgentPhaseable
111
+ #
112
+ # def allowed_phases
113
+ # %w[scoping research writing review]
114
+ # end
115
+ # end
116
+ #
117
+ # @return [Array<String>] List of allowed phase names
118
+ def allowed_phases
119
+ # Default: allow any phase
120
+ # Override in your model to restrict to specific phases
121
+ nil
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Observ
4
+ # Interface contract for agents that can be selected in the Observ UI
5
+ # This module defines the required methods that any selectable agent must implement
6
+ #
7
+ # Usage:
8
+ # class MyAgent < BaseAgent
9
+ # include Observ::AgentSelectable
10
+ #
11
+ # def self.display_name
12
+ # "My Custom Agent"
13
+ # end
14
+ #
15
+ # def self.description
16
+ # "Description of what this agent does"
17
+ # end
18
+ # end
19
+ module AgentSelectable
20
+ extend ActiveSupport::Concern
21
+
22
+ class_methods do
23
+ # Required: Human-readable name displayed in the UI
24
+ # @return [String] the display name for the agent
25
+ def display_name
26
+ raise NotImplementedError, "#{name} must implement .display_name class method"
27
+ end
28
+
29
+ # Required: Unique identifier for the agent (typically the class name)
30
+ # Used as the value in select dropdowns
31
+ # @return [String] the agent identifier
32
+ def agent_identifier
33
+ name
34
+ end
35
+
36
+ # Optional: Description of what the agent does
37
+ # Displayed as help text or tooltips in the UI
38
+ # @return [String, nil] the agent description
39
+ def description
40
+ nil
41
+ end
42
+
43
+ # Optional: Category for grouping agents in the UI
44
+ # @return [String] the category name
45
+ def category
46
+ "General"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Observ
4
+ # Concern for enhancing Chat models with observability and agent support
5
+ # This provides the integration between your Chat model and the Observ system
6
+ #
7
+ # Usage:
8
+ # class Chat < ApplicationRecord
9
+ # include Observ::ChatEnhancements
10
+ #
11
+ # # Optional: Define agent_class method if using agents
12
+ # def agent_class
13
+ # return BaseAgent if agent_class_name.blank?
14
+ # agent_class_name.constantize
15
+ # end
16
+ # end
17
+ module ChatEnhancements
18
+ extend ActiveSupport::Concern
19
+
20
+ included do
21
+ include Observ::ObservabilityInstrumentation
22
+
23
+ # Set the model from agent BEFORE RubyLLM's resolve_model_from_strings runs
24
+ # This ensures the prompt version override is applied when determining the model
25
+ before_save :set_model_from_agent, if: -> { respond_to?(:agent_class_name) && agent_class_name.present? }
26
+
27
+ # Initialize agent on creation (includes greeting message)
28
+ after_create :initialize_agent_on_create, if: -> { respond_to?(:agent_class_name) && agent_class_name.present? }
29
+ end
30
+
31
+ # Setup tools for the chat session
32
+ # Override this in your Chat model if you need custom tool setup
33
+ def setup_tools
34
+ return unless respond_to?(:agent_class)
35
+ agent_class.setup_tools(self)
36
+ end
37
+
38
+ # Ensure agent parameters are set when needed
39
+ # This is called lazily when the chat is actually used
40
+ #
41
+ # Note: Instructions are already set in initialize_agent_on_create (after_create callback)
42
+ # and should NOT be re-applied on every message. Only parameters need to be re-set
43
+ # because they are lost when the chat is reloaded from the database.
44
+ def ensure_agent_configured
45
+ return unless respond_to?(:agent_class) && agent_class_name.present?
46
+ return if @_agent_params_configured
47
+
48
+ # Set prompt version override if specified
49
+ if respond_to?(:prompt_version) && prompt_version.present? &&
50
+ agent_class.included_modules.include?(Observ::PromptManagement)
51
+ Thread.current[:observ_prompt_version_override] = prompt_version
52
+ end
53
+
54
+ begin
55
+ # Only re-apply parameters, not instructions
56
+ # Instructions were already set at creation time
57
+ agent_class.setup_parameters(self)
58
+ @_agent_params_configured = true
59
+ ensure
60
+ Thread.current[:observ_prompt_version_override] = nil
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ # Set the model from agent configuration before save
67
+ # This runs BEFORE RubyLLM's resolve_model_from_strings callback,
68
+ # ensuring the correct model is used when a specific prompt version is specified
69
+ def set_model_from_agent
70
+ return unless respond_to?(:agent_class)
71
+
72
+ # Set prompt version override if specified and agent supports prompt management
73
+ if respond_to?(:prompt_version) && prompt_version.present? &&
74
+ agent_class.included_modules.include?(Observ::PromptManagement)
75
+ Thread.current[:observ_prompt_version_override] = prompt_version
76
+ end
77
+
78
+ begin
79
+ # Set the model string so RubyLLM's resolve_model_from_strings uses the correct model
80
+ # This uses the agent's model method which respects the prompt version override
81
+ @model_string = agent_class.model
82
+ ensure
83
+ Thread.current[:observ_prompt_version_override] = nil
84
+ end
85
+ end
86
+
87
+ def initialize_agent_on_create
88
+ return unless respond_to?(:agent_class)
89
+
90
+ # If chat has a specific prompt version, temporarily set it for setup
91
+ if respond_to?(:prompt_version) && prompt_version.present? &&
92
+ agent_class.included_modules.include?(Observ::PromptManagement)
93
+ # Store the version temporarily so setup_instructions can use it
94
+ Thread.current[:observ_prompt_version_override] = prompt_version
95
+ end
96
+
97
+ begin
98
+ # Execute all agent setup steps in one consolidated callback
99
+ # This prevents redundant association loading between callbacks
100
+ agent_class.setup_instructions(self)
101
+ agent_class.setup_parameters(self)
102
+ agent_class.send_initial_greeting(self)
103
+ ensure
104
+ # Clean up thread-local storage
105
+ Thread.current[:observ_prompt_version_override] = nil
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Observ
4
+ # Concern for enhancing Message models with observability and broadcasting support
5
+ # This provides the integration between your Message model and the Observ system
6
+ #
7
+ # Usage:
8
+ # class Message < ApplicationRecord
9
+ # include Observ::MessageEnhancements
10
+ # end
11
+ module MessageEnhancements
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ include Observ::TraceAssociation
16
+
17
+ # Broadcasts message updates to the chat channel
18
+ # Override the lambda if your chat_id attribute has a different name
19
+ broadcasts_to ->(message) { "chat_#{message.chat_id}" }, partial: "observ/messages/message"
20
+ end
21
+
22
+ # Broadcast a content chunk to the message
23
+ # Useful for streaming responses
24
+ def broadcast_append_chunk(content)
25
+ broadcast_append_to "chat_#{chat_id}",
26
+ target: "message_#{id}_content",
27
+ partial: "observ/messages/content",
28
+ locals: { content: content }
29
+ end
30
+ end
31
+ end