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,285 @@
1
+ <% content_for :title, "Trace #{truncate_id(@trace.trace_id, 12)}" %>
2
+
3
+ <% content_for :page_header do %>
4
+ <div class="observ-page-header__content">
5
+ <div class="observ-page-header__breadcrumb">
6
+ <%= link_to "Traces", traces_path, class: "observ-link" %> /
7
+ </div>
8
+ <h1 class="observ-page-header__title">
9
+ Trace <code class="observ-code observ-code--inline"><%= truncate_id(@trace.trace_id, 12) %></code>
10
+ </h1>
11
+ <div class="observ-page-header__meta">
12
+ <%= observ_status_badge(observ_trace_status(@trace)) %>
13
+ </div>
14
+ </div>
15
+ <div class="observ-page-header__actions">
16
+ <%= link_to "#",
17
+ class: "observ-button observ-button--secondary observ-button--sm",
18
+ data: {
19
+ action: "click->observ--drawer#open",
20
+ drawer_url_param: text_output_drawer_trace_path(@trace)
21
+ } do %>
22
+ See Text Output
23
+ <% end %>
24
+
25
+ <%= link_to "#",
26
+ class: "observ-button observ-button--secondary observ-button--sm",
27
+ data: {
28
+ action: "click->observ--drawer#open",
29
+ drawer_url_param: add_to_dataset_drawer_trace_path(@trace)
30
+ } do %>
31
+ Add to Dataset
32
+ <% end %>
33
+
34
+ <%= link_to "#",
35
+ class: "observ-button observ-button--primary observ-button--sm",
36
+ data: {
37
+ action: "click->observ--drawer#open",
38
+ drawer_url_param: annotations_drawer_trace_path(@trace)
39
+ } do %>
40
+ Annotate
41
+ <% if @trace.annotations.any? %>
42
+ <span class="observ-badge observ-badge--light"><%= @trace.annotations.count %></span>
43
+ <% end %>
44
+ <% end %>
45
+ </div>
46
+ <% end %>
47
+
48
+ <div class="observ-container">
49
+ <section class="observ-card observ-card--compact">
50
+ <header class="observ-card__header">
51
+ <h2 class="observ-card__title">Details</h2>
52
+ </header>
53
+ <div class="observ-card__body">
54
+ <dl class="observ-definition-list observ-definition-list--compact">
55
+ <div class="observ-definition-list__item">
56
+ <dt class="observ-definition-list__term">Session</dt>
57
+ <dd class="observ-definition-list__definition">
58
+ <%= link_to truncate_id(@trace.observ_session.session_id, 12), session_path(@trace.observ_session), class: "observ-link" %>
59
+ </dd>
60
+ </div>
61
+ <div class="observ-definition-list__item">
62
+ <dt class="observ-definition-list__term">Name</dt>
63
+ <dd class="observ-definition-list__definition"><%= @trace.name || "—" %></dd>
64
+ </div>
65
+ <div class="observ-definition-list__item">
66
+ <dt class="observ-definition-list__term">Start Time</dt>
67
+ <dd class="observ-definition-list__definition"><%= observ_timestamp(@trace.start_time) %></dd>
68
+ </div>
69
+ <% if @trace.end_time %>
70
+ <div class="observ-definition-list__item">
71
+ <dt class="observ-definition-list__term">End Time</dt>
72
+ <dd class="observ-definition-list__definition"><%= observ_timestamp(@trace.end_time) %></dd>
73
+ </div>
74
+ <% end %>
75
+ <div class="observ-definition-list__item">
76
+ <dt class="observ-definition-list__term">Duration</dt>
77
+ <dd class="observ-definition-list__definition">
78
+ <% if @trace.duration_ms %>
79
+ <%= format_duration_ms(@trace.duration_ms) %>
80
+ <% else %>
81
+ <span class="observ-text--muted">In Progress</span>
82
+ <% end %>
83
+ </dd>
84
+ </div>
85
+ <div class="observ-definition-list__item">
86
+ <dt class="observ-definition-list__term">Total Tokens</dt>
87
+ <dd class="observ-definition-list__definition"><%= format_tokens(@trace.total_tokens) %></dd>
88
+ </div>
89
+ <div class="observ-definition-list__item">
90
+ <dt class="observ-definition-list__term">Total Cost</dt>
91
+ <dd class="observ-definition-list__definition"><%= format_currency(@trace.total_cost) %></dd>
92
+ </div>
93
+ </dl>
94
+ </div>
95
+ </section>
96
+
97
+ <% if @trace.input.present? %>
98
+ <section class="observ-card observ-card--compact">
99
+ <header class="observ-card__header">
100
+ <h2 class="observ-card__title">Input</h2>
101
+ </header>
102
+ <div class="observ-card__body">
103
+ <%= render_input_output(@trace.input, compact: true) %>
104
+ </div>
105
+ </section>
106
+ <% end %>
107
+
108
+ <% if @trace.output.present? %>
109
+ <section class="observ-card observ-card--compact">
110
+ <header class="observ-card__header">
111
+ <h2 class="observ-card__title">Output</h2>
112
+ </header>
113
+ <div class="observ-card__body">
114
+ <%= render_input_output(@trace.output, compact: true) %>
115
+ </div>
116
+ </section>
117
+ <% end %>
118
+
119
+ <% if @trace.metadata.present? && @trace.metadata.any? %>
120
+ <section class="observ-card observ-card--compact">
121
+ <header class="observ-card__header">
122
+ <h2 class="observ-card__title">Metadata</h2>
123
+ </header>
124
+ <div class="observ-card__body">
125
+ <%= render_json_viewer(@trace.metadata, compact: true) %>
126
+ </div>
127
+ </section>
128
+ <% end %>
129
+
130
+ <section class="observ-card">
131
+ <header class="observ-card__header">
132
+ <h2 class="observ-card__title">Observations (<%= @observations.count %>)</h2>
133
+ </header>
134
+ <div class="observ-card__body">
135
+ <% if @observations.any? %>
136
+ <div class="observ-observations-list observ-observations-list--compact">
137
+ <% @observations.each do |observation| %>
138
+ <div class="observ-observation-card observ-observation-card--compact observ-observation-card--<%= observation.type.demodulize.downcase %>">
139
+ <div class="observ-observation-card__header">
140
+ <span class="observ-observation-card__type-badge">
141
+ <%= observation.type.demodulize %>
142
+ </span>
143
+ <h3 class="observ-observation-card__name"><%= observation.name %></h3>
144
+ <div class="observ-observation-card__meta">
145
+ <span class="observ-observation-card__id">
146
+ <code class="observ-code observ-code--inline"><%= truncate_id(observation.observation_id, 8) %></code>
147
+ </span>
148
+ </div>
149
+ </div>
150
+
151
+ <div class="observ-observation-card__body">
152
+ <dl class="observ-definition-list observ-definition-list--horizontal observ-definition-list--compact">
153
+ <% if observation.is_a?(Observ::Generation) %>
154
+ <div class="observ-definition-list__item">
155
+ <dt class="observ-definition-list__term">Model</dt>
156
+ <dd class="observ-definition-list__definition">
157
+ <%= observ_model_badge(observation.model) %>
158
+ </dd>
159
+ </div>
160
+ <div class="observ-definition-list__item">
161
+ <dt class="observ-definition-list__term">Tokens</dt>
162
+ <dd class="observ-definition-list__definition">
163
+ <%= format_tokens(observation.total_tokens) %>
164
+ </dd>
165
+ </div>
166
+ <div class="observ-definition-list__item">
167
+ <dt class="observ-definition-list__term">Cost</dt>
168
+ <dd class="observ-definition-list__definition">
169
+ <%= format_currency(observation.cost_usd) %>
170
+ </dd>
171
+ </div>
172
+ <% if observation.finish_reason %>
173
+ <div class="observ-definition-list__item">
174
+ <dt class="observ-definition-list__term">Finish</dt>
175
+ <dd class="observ-definition-list__definition">
176
+ <%= observation.finish_reason %>
177
+ </dd>
178
+ </div>
179
+ <% end %>
180
+ <% if observation.time_to_first_token_ms %>
181
+ <div class="observ-definition-list__item">
182
+ <dt class="observ-definition-list__term">TTFT</dt>
183
+ <dd class="observ-definition-list__definition">
184
+ <%= format_duration_ms(observation.time_to_first_token_ms) %>
185
+ </dd>
186
+ </div>
187
+ <% end %>
188
+ <div class="observ-definition-list__item">
189
+ <dt class="observ-definition-list__term">Prompt</dt>
190
+ <dd class="observ-definition-list__definition">
191
+ <% if observation.prompt_name.present? %>
192
+ <%= observation.prompt_name %>
193
+ <% if observation.prompt_version.present? %>
194
+ <span class="observ-text--muted">(v<%= observation.prompt_version %>)</span>
195
+ <% end %>
196
+ <% else %>
197
+ <span class="observ-text--muted">default prompt</span>
198
+ <% end %>
199
+ </dd>
200
+ </div>
201
+ <% end %>
202
+ <div class="observ-definition-list__item">
203
+ <dt class="observ-definition-list__term">Duration</dt>
204
+ <dd class="observ-definition-list__definition">
205
+ <% if observation.duration_ms %>
206
+ <%= format_duration_ms(observation.duration_ms) %>
207
+ <% else %>
208
+ <span class="observ-text--muted">—</span>
209
+ <% end %>
210
+ </dd>
211
+ </div>
212
+ <% if observation.is_a?(Observ::Span) && observation.status_message %>
213
+ <div class="observ-definition-list__item">
214
+ <dt class="observ-definition-list__term">Status</dt>
215
+ <dd class="observ-definition-list__definition">
216
+ <%= observation.status_message %>
217
+ </dd>
218
+ </div>
219
+ <% end %>
220
+ </dl>
221
+ </div>
222
+
223
+ <% if observation.is_a?(Observ::Generation) && observation.usage.present? && observation.usage.any? %>
224
+ <div class="observ-observation-card__section observ-observation-card__section--compact">
225
+ <h4 class="observ-observation-card__section-title">Token Usage</h4>
226
+ <dl class="observ-definition-list observ-definition-list--horizontal observ-definition-list--compact">
227
+ <% observation.usage.each do |key, value| %>
228
+ <div class="observ-definition-list__item">
229
+ <dt class="observ-definition-list__term"><%= key.humanize %></dt>
230
+ <dd class="observ-definition-list__definition"><%= format_tokens(value) %></dd>
231
+ </div>
232
+ <% end %>
233
+ </dl>
234
+ </div>
235
+ <% end %>
236
+
237
+ <% if observation.input.present? %>
238
+ <div class="observ-observation-card__section observ-observation-card__section--compact">
239
+ <h4 class="observ-observation-card__section-title">Input</h4>
240
+ <pre class="observ-code-block observ-code-block--compact"><%= observation.input %></pre>
241
+ </div>
242
+ <% end %>
243
+
244
+ <% if observation.output.present? %>
245
+ <div class="observ-observation-card__section observ-observation-card__section--compact">
246
+ <h4 class="observ-observation-card__section-title">Output</h4>
247
+ <pre class="observ-code-block observ-code-block--compact"><%= observation.output %></pre>
248
+ </div>
249
+ <% end %>
250
+
251
+ <% if observation.is_a?(Observ::Generation) && observation.messages.present? && observation.messages.any? %>
252
+ <div class="observ-observation-card__section observ-observation-card__section--compact">
253
+ <h4 class="observ-observation-card__section-title">Messages</h4>
254
+ <%= render_json_viewer(observation.messages, compact: true) %>
255
+ </div>
256
+ <% end %>
257
+
258
+ <% if observation.is_a?(Observ::Generation) && observation.provider_metadata.present? && observation.provider_metadata.any? %>
259
+ <div class="observ-observation-card__section observ-observation-card__section--compact">
260
+ <h4 class="observ-observation-card__section-title">Provider Metadata</h4>
261
+ <%= render_json_viewer(observation.provider_metadata, compact: true) %>
262
+ </div>
263
+ <% end %>
264
+
265
+ <% if observation.is_a?(Observ::Span) && observation.metadata.present? && observation.metadata.any? %>
266
+ <div class="observ-observation-card__section observ-observation-card__section--compact">
267
+ <h4 class="observ-observation-card__section-title">Metadata</h4>
268
+ <%= render_json_viewer(observation.metadata, compact: true) %>
269
+ </div>
270
+ <% end %>
271
+
272
+ <div class="observ-observation-card__footer">
273
+ <%= link_to "View Details →", observation_path(observation), class: "observ-link" %>
274
+ </div>
275
+ </div>
276
+ <% end %>
277
+ </div>
278
+ <% else %>
279
+ <div class="observ-card__empty">
280
+ <p>No observations found for this trace.</p>
281
+ </div>
282
+ <% end %>
283
+ </div>
284
+ </section>
285
+ </div>
@@ -0,0 +1,48 @@
1
+ <%= turbo_stream.update "drawer-header-title" do %>
2
+ Trace Text Output
3
+ <% end %>
4
+
5
+ <%= turbo_stream.update "drawer-content" do %>
6
+ <div class="observ-drawer__section">
7
+ <div class="observ-drawer__instructions">
8
+ <strong>💡 Quick Start:</strong> Click the textarea to select all text, then copy and paste it into ChatGPT, Claude, or any LLM for analysis.
9
+ </div>
10
+
11
+ <div
12
+ class="observ-drawer__text-output"
13
+ data-controller="observ--text-select observ--copy"
14
+ >
15
+ <textarea
16
+ class="observ-drawer__textarea"
17
+ readonly
18
+ data-observ--text-select-target="text"
19
+ data-observ--copy-target="source"
20
+ data-action="click->observ--text-select#select"
21
+ aria-label="Formatted trace text output"
22
+ ><%= @formatted_text %></textarea>
23
+
24
+ <button
25
+ type="button"
26
+ class="observ-button observ-button--sm observ-button--secondary observ-drawer__copy-btn"
27
+ data-observ--copy-target="button"
28
+ data-action="click->observ--copy#copy"
29
+ >
30
+ 📋 Copy to Clipboard
31
+ </button>
32
+ </div>
33
+
34
+ <div class="observ-drawer__help">
35
+ <details>
36
+ <summary>What can you do with this text?</summary>
37
+ <ul class="observ-drawer__help-list">
38
+ <li>Paste into ChatGPT/Claude for performance analysis</li>
39
+ <li>Ask LLMs to identify optimization opportunities</li>
40
+ <li>Get suggestions for cost reduction strategies</li>
41
+ <li>Analyze error patterns and get debugging suggestions</li>
42
+ <li>Review quality and accuracy of AI responses</li>
43
+ <li>Generate reports or documentation from trace data</li>
44
+ </ul>
45
+ </details>
46
+ </div>
47
+ </div>
48
+ <% end %>
@@ -0,0 +1,26 @@
1
+ <div class="observ-drawer" data-observ--drawer-target="drawer">
2
+ <div class="observ-drawer__overlay" data-action="click->observ--drawer#close"></div>
3
+
4
+ <div class="observ-drawer__panel">
5
+ <div class="observ-drawer__header">
6
+ <button
7
+ type="button"
8
+ class="observ-drawer__close-btn"
9
+ data-action="click->observ--drawer#close"
10
+ aria-label="Close drawer">
11
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
12
+ <path d="M15 5L5 15M5 5L15 15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
13
+ </svg>
14
+ </button>
15
+ <h2 id="drawer-header-title" class="observ-drawer__title" data-observ--drawer-target="headerTitle">
16
+ Loading...
17
+ </h2>
18
+ </div>
19
+
20
+ <div id="drawer-content" class="observ-drawer__content" data-observ--drawer-target="content">
21
+ <div class="observ-drawer__loading">
22
+ <span class="observ-spinner"></span>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
data/config/routes.rb ADDED
@@ -0,0 +1,80 @@
1
+ Observ::Engine.routes.draw do
2
+ root to: "dashboard#index"
3
+
4
+ get "dashboard", to: "dashboard#index", as: :dashboard
5
+ get "dashboard/metrics", to: "dashboard#metrics"
6
+ get "dashboard/cost_analysis", to: "dashboard#cost_analysis"
7
+
8
+ # Chat routes - only available if Chat model exists in host app
9
+ if defined?(::Chat) && ::Chat.respond_to?(:acts_as_chat)
10
+ resources :chats, only: [ :index, :new, :create, :show ] do
11
+ resources :messages, only: [ :create ]
12
+ end
13
+ end
14
+
15
+ resources :sessions, only: [ :index, :show ] do
16
+ member do
17
+ get :metrics
18
+ get :drawer_test
19
+ get :annotations_drawer
20
+ end
21
+ resources :annotations, only: [ :index, :create, :destroy ]
22
+ end
23
+
24
+ resources :traces, only: [ :index, :show ] do
25
+ collection do
26
+ get :search
27
+ end
28
+ member do
29
+ get :annotations_drawer
30
+ get :text_output_drawer
31
+ get :add_to_dataset_drawer
32
+ post :add_to_dataset
33
+ end
34
+ resources :annotations, only: [ :index, :create, :destroy ]
35
+ end
36
+
37
+ resources :observations, only: [ :index, :show ] do
38
+ collection do
39
+ get :generations
40
+ get :spans
41
+ end
42
+ end
43
+
44
+ get "annotations/sessions", to: "annotations#sessions_index", as: :sessions_annotations
45
+ get "annotations/traces", to: "annotations#traces_index", as: :traces_annotations
46
+ get "annotations/export", to: "annotations#export", as: :export_annotations
47
+
48
+ resources :prompts do
49
+ member do
50
+ get :versions # Version history view
51
+ get :compare # Compare versions
52
+ end
53
+
54
+ resources :versions, only: [ :show ], controller: "prompt_versions" do
55
+ member do
56
+ post :promote # draft -> production
57
+ post :demote # production -> archived
58
+ post :restore # archived -> production
59
+ post :clone # create editable draft copy
60
+ end
61
+ end
62
+ end
63
+
64
+ resources :datasets do
65
+ resources :items, controller: "dataset_items", except: [ :show ]
66
+ resources :runs, controller: "dataset_runs", only: [ :index, :show, :new, :create, :destroy ] do
67
+ member do
68
+ post :run_evaluators
69
+ get :review
70
+ end
71
+ resources :run_items, controller: "dataset_run_items", only: [] do
72
+ member do
73
+ get :details_drawer
74
+ get :score_drawer
75
+ post :score
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,21 @@
1
+ class CreateObservSessions < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :observ_sessions do |t|
4
+ t.string :session_id, null: false
5
+ t.string :user_id
6
+ t.datetime :start_time, null: false
7
+ t.datetime :end_time
8
+ t.json :metadata, default: {}
9
+ t.integer :total_traces_count, default: 0
10
+ t.integer :total_llm_calls_count, default: 0
11
+ t.integer :total_tokens, default: 0
12
+ t.decimal :total_cost, precision: 10, scale: 6, default: 0.0
13
+ t.integer :total_llm_duration_ms, default: 0
14
+
15
+ t.timestamps
16
+ end
17
+ add_index :observ_sessions, :session_id, unique: true
18
+ add_index :observ_sessions, :user_id
19
+ add_index :observ_sessions, :start_time
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ class CreateObservTraces < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :observ_traces do |t|
4
+ t.string :trace_id, null: false
5
+ t.references :observ_session, null: false, foreign_key: true
6
+ t.string :name
7
+ t.datetime :start_time, null: false
8
+ t.datetime :end_time
9
+ t.text :input
10
+ t.text :output
11
+ t.json :metadata, default: {}
12
+ t.json :tags, default: []
13
+ t.string :user_id
14
+ t.string :release
15
+ t.string :version
16
+ t.decimal :total_cost, precision: 10, scale: 6, default: 0.0
17
+ t.integer :total_tokens, default: 0
18
+
19
+ t.timestamps
20
+ end
21
+ add_index :observ_traces, :trace_id, unique: true
22
+ add_index :observ_traces, :name
23
+ add_index :observ_traces, :start_time
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+ class CreateObservObservations < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :observ_observations do |t|
4
+ t.string :observation_id, null: false
5
+ t.references :observ_trace, null: false, foreign_key: true
6
+ t.string :parent_observation_id
7
+ t.string :type, null: false
8
+ t.string :name
9
+ t.datetime :start_time, null: false
10
+ t.datetime :end_time
11
+ t.json :metadata, default: {}
12
+ t.string :level, default: 'DEFAULT'
13
+ t.string :status_message
14
+ t.string :version
15
+
16
+ # Generation-specific fields
17
+ t.string :model
18
+ t.json :model_parameters, default: {}
19
+ t.text :input
20
+ t.text :output
21
+ t.json :usage, default: {}
22
+ t.decimal :cost_usd, precision: 10, scale: 6
23
+ t.string :prompt_name
24
+ t.string :prompt_version
25
+ t.datetime :completion_start_time
26
+ t.string :finish_reason
27
+ t.json :provider_metadata, default: {}
28
+ t.json :messages, default: []
29
+ t.json :tools, default: []
30
+ t.string :tool_choice
31
+ t.json :raw_response
32
+
33
+ t.timestamps
34
+ end
35
+
36
+ add_index :observ_observations, :observation_id, unique: true
37
+ add_index :observ_observations, :parent_observation_id
38
+ add_index :observ_observations, :type
39
+ add_index :observ_observations, :name
40
+ add_index :observ_observations, :start_time
41
+ end
42
+ end
@@ -0,0 +1,7 @@
1
+ class AddMessageIdToObservTraces < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :observ_traces, :message_id, :integer
4
+ add_index :observ_traces, :message_id
5
+ add_foreign_key :observ_traces, :messages
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ class CreateObservPrompts < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :observ_prompts do |t|
4
+ t.string :name, null: false
5
+ t.text :prompt, null: false
6
+ t.integer :version, null: false
7
+ t.string :state, null: false, default: 'draft'
8
+ t.json :config, default: {}
9
+ t.text :commit_message
10
+ t.string :created_by
11
+
12
+ t.timestamps
13
+
14
+ # Composite unique index for name + version
15
+ t.index [ :name, :version ], unique: true
16
+
17
+ # Index for state queries (e.g., find all production prompts)
18
+ t.index [ :name, :state ]
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ class FixPromptConfigStrings < ActiveRecord::Migration[7.0]
2
+ def up
3
+ # Find all prompts where config is stored as a String instead of JSON
4
+ Observ::Prompt.find_each do |prompt|
5
+ next if prompt.config.nil?
6
+
7
+ if prompt.config.is_a?(String)
8
+ begin
9
+ parsed = JSON.parse(prompt.config)
10
+ prompt.update_column(:config, parsed)
11
+ puts "Fixed config for #{prompt.name} v#{prompt.version}"
12
+ rescue JSON::ParserError => e
13
+ puts "WARNING: Could not parse config for #{prompt.name} v#{prompt.version}: #{e.message}"
14
+ prompt.update_column(:config, {})
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ def down
21
+ # No need to reverse this - we're fixing data corruption
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ class CreateObservAnnotations < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :observ_annotations do |t|
4
+ t.references :annotatable, polymorphic: true, null: false, index: true
5
+ t.text :content, null: false
6
+ t.string :annotator
7
+ t.text :tags
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddPromptFieldsToObservChats < ActiveRecord::Migration[7.0]
4
+ def change
5
+ # Only add columns if chats table exists (host app may not have chat feature)
6
+ return unless table_exists?(:chats)
7
+
8
+ add_column :chats, :prompt_name, :string, null: true
9
+ add_column :chats, :prompt_version, :integer, null: true
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateObservDatasets < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :observ_datasets do |t|
6
+ t.string :name, null: false
7
+ t.text :description
8
+ t.string :agent_class, null: false
9
+ t.json :metadata, default: {}
10
+ t.timestamps
11
+
12
+ t.index :name, unique: true
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateObservDatasetItems < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :observ_dataset_items do |t|
6
+ t.references :dataset, null: false, foreign_key: { to_table: :observ_datasets }
7
+ t.integer :status, default: 0, null: false
8
+ t.json :input, null: false
9
+ t.json :expected_output
10
+ t.json :metadata, default: {}
11
+ t.references :source_trace, foreign_key: { to_table: :observ_traces }
12
+ t.timestamps
13
+
14
+ t.index [ :dataset_id, :status ]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateObservDatasetRuns < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :observ_dataset_runs do |t|
6
+ t.references :dataset, null: false, foreign_key: { to_table: :observ_datasets }
7
+ t.string :name, null: false
8
+ t.text :description
9
+ t.integer :status, default: 0, null: false
10
+ t.json :metadata, default: {}
11
+ t.integer :total_items, default: 0
12
+ t.integer :completed_items, default: 0
13
+ t.integer :failed_items, default: 0
14
+ t.decimal :total_cost, precision: 10, scale: 6, default: 0
15
+ t.integer :total_tokens, default: 0
16
+ t.timestamps
17
+
18
+ t.index [ :dataset_id, :name ], unique: true
19
+ t.index [ :dataset_id, :status ]
20
+ end
21
+ end
22
+ end