rubyllm-observ 0.5.1 → 0.6.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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +54 -6
  3. data/app/assets/stylesheets/observ/_annotations.scss +114 -103
  4. data/app/assets/stylesheets/observ/_card.scss +58 -49
  5. data/app/assets/stylesheets/observ/_chat.scss +247 -155
  6. data/app/assets/stylesheets/observ/_components.scss +622 -340
  7. data/app/assets/stylesheets/observ/_dashboard.scss +31 -28
  8. data/app/assets/stylesheets/observ/_datasets.scss +494 -547
  9. data/app/assets/stylesheets/observ/_drawer.scss +250 -228
  10. data/app/assets/stylesheets/observ/_filters.scss +139 -0
  11. data/app/assets/stylesheets/observ/_json_viewer.scss +103 -97
  12. data/app/assets/stylesheets/observ/_layout.scss +443 -178
  13. data/app/assets/stylesheets/observ/_metrics.scss +79 -76
  14. data/app/assets/stylesheets/observ/_namespace.scss +18 -0
  15. data/app/assets/stylesheets/observ/_observations.scss +122 -119
  16. data/app/assets/stylesheets/observ/_pagination.scss +129 -112
  17. data/app/assets/stylesheets/observ/_prompts.scss +485 -269
  18. data/app/assets/stylesheets/observ/_reset.scss +249 -0
  19. data/app/assets/stylesheets/observ/_table.scss +46 -38
  20. data/app/assets/stylesheets/observ/_variables.scss +54 -0
  21. data/app/assets/stylesheets/observ/application.scss +3 -0
  22. data/app/controllers/observ/dataset_run_items_controller.rb +0 -1
  23. data/app/controllers/observ/review_queue_controller.rb +154 -0
  24. data/app/controllers/observ/scores_controller.rb +64 -0
  25. data/app/controllers/observ/sessions_controller.rb +23 -0
  26. data/app/helpers/observ/application_helper.rb +1 -0
  27. data/app/helpers/observ/reviews_helper.rb +33 -0
  28. data/app/models/concerns/observ/json_queryable.rb +138 -0
  29. data/app/models/concerns/observ/reviewable.rb +41 -0
  30. data/app/models/concerns/observ/scoreable.rb +34 -0
  31. data/app/models/observ/dataset_run_item.rb +3 -13
  32. data/app/models/observ/review_item.rb +48 -0
  33. data/app/models/observ/score.rb +38 -6
  34. data/app/models/observ/session.rb +5 -1
  35. data/app/models/observ/trace.rb +3 -0
  36. data/app/services/observ/evaluators/base_evaluator.rb +0 -1
  37. data/app/services/observ/guardrail_service.rb +128 -0
  38. data/app/views/kaminari/_first_page.html.erb +1 -1
  39. data/app/views/kaminari/_gap.html.erb +1 -1
  40. data/app/views/kaminari/_last_page.html.erb +1 -1
  41. data/app/views/kaminari/_next_page.html.erb +1 -1
  42. data/app/views/kaminari/_page.html.erb +1 -1
  43. data/app/views/kaminari/_paginator.html.erb +1 -1
  44. data/app/views/kaminari/_prev_page.html.erb +1 -1
  45. data/app/views/kaminari/observ/_first_page.html.erb +1 -1
  46. data/app/views/kaminari/observ/_gap.html.erb +1 -1
  47. data/app/views/kaminari/observ/_last_page.html.erb +1 -1
  48. data/app/views/kaminari/observ/_next_page.html.erb +1 -1
  49. data/app/views/kaminari/observ/_page.html.erb +1 -1
  50. data/app/views/kaminari/observ/_paginator.html.erb +1 -1
  51. data/app/views/kaminari/observ/_prev_page.html.erb +1 -1
  52. data/app/views/layouts/observ/application.html.erb +96 -58
  53. data/app/views/observ/annotations/_form.html.erb +5 -5
  54. data/app/views/observ/annotations/index.html.erb +4 -4
  55. data/app/views/observ/annotations/sessions_index.html.erb +9 -9
  56. data/app/views/observ/annotations/traces_index.html.erb +9 -9
  57. data/app/views/observ/chats/_form.html.erb +7 -7
  58. data/app/views/observ/datasets/index.html.erb +6 -6
  59. data/app/views/observ/messages/_form.html.erb +11 -12
  60. data/app/views/observ/observations/index.html.erb +3 -4
  61. data/app/views/observ/prompts/_form.html.erb +37 -38
  62. data/app/views/observ/prompts/_new_form.html.erb +37 -38
  63. data/app/views/observ/prompts/compare.html.erb +59 -55
  64. data/app/views/observ/prompts/edit.html.erb +3 -3
  65. data/app/views/observ/prompts/index.html.erb +9 -9
  66. data/app/views/observ/prompts/new.html.erb +3 -3
  67. data/app/views/observ/prompts/show.html.erb +2 -2
  68. data/app/views/observ/prompts/versions.html.erb +22 -22
  69. data/app/views/observ/review_queue/_item.html.erb +39 -0
  70. data/app/views/observ/review_queue/_stats.html.erb +18 -0
  71. data/app/views/observ/review_queue/index.html.erb +49 -0
  72. data/app/views/observ/review_queue/show.html.erb +76 -0
  73. data/app/views/observ/review_queue/stats.html.erb +100 -0
  74. data/app/views/observ/scores/_form.html.erb +39 -0
  75. data/app/views/observ/scores/create.turbo_stream.erb +10 -0
  76. data/app/views/observ/sessions/_chat.html.erb +59 -0
  77. data/app/views/observ/sessions/_metadata.html.erb +17 -0
  78. data/app/views/observ/sessions/_metrics.html.erb +81 -0
  79. data/app/views/observ/sessions/_traces.html.erb +92 -0
  80. data/app/views/observ/sessions/annotations_drawer.turbo_stream.erb +8 -1
  81. data/app/views/observ/sessions/index.html.erb +60 -4
  82. data/app/views/observ/sessions/show.html.erb +4 -217
  83. data/app/views/observ/traces/_details.html.erb +47 -0
  84. data/app/views/observ/traces/_input.html.erb +10 -0
  85. data/app/views/observ/traces/_metadata.html.erb +10 -0
  86. data/app/views/observ/traces/_observations.html.erb +172 -0
  87. data/app/views/observ/traces/_output.html.erb +10 -0
  88. data/app/views/observ/traces/annotations_drawer.turbo_stream.erb +8 -1
  89. data/app/views/observ/traces/index.html.erb +3 -4
  90. data/app/views/observ/traces/show.html.erb +5 -232
  91. data/config/routes.rb +14 -0
  92. data/db/migrate/015_refactor_scores_to_polymorphic.rb +27 -0
  93. data/db/migrate/016_create_observ_review_items.rb +25 -0
  94. data/lib/observ/version.rb +1 -1
  95. metadata +30 -1
@@ -0,0 +1,249 @@
1
+ // Observ CSS Reset
2
+ // ================
3
+ // This file provides a scoped CSS reset for form elements within Observ
4
+ // to ensure they are not affected by styles from the host application.
5
+ //
6
+ // IMPORTANT: This reset is intentionally minimal and focused on FORM ELEMENTS ONLY.
7
+ // Structural elements (lists, tables, headings) are styled by Observ's component stylesheets.
8
+
9
+ @import 'variables';
10
+
11
+ .observ-layout {
12
+ // ============================================
13
+ // Box Sizing Reset (applied to all elements)
14
+ // ============================================
15
+ *,
16
+ *::before,
17
+ *::after {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ // ============================================
22
+ // Form Elements Reset
23
+ // ============================================
24
+ // These resets ensure form elements have consistent styling
25
+ // regardless of the host app's global CSS
26
+
27
+ // Labels - reset font to inherit from parent
28
+ label {
29
+ font-family: inherit;
30
+ font-size: inherit;
31
+ line-height: inherit;
32
+ text-transform: none;
33
+ }
34
+
35
+ // Buttons - full reset to prevent host app interference
36
+ button {
37
+ font-family: inherit;
38
+ font-size: inherit;
39
+ line-height: inherit;
40
+ background: none;
41
+ border: none;
42
+ padding: 0;
43
+ margin: 0;
44
+ cursor: pointer;
45
+ appearance: none;
46
+ -webkit-appearance: none;
47
+
48
+ &:focus {
49
+ outline: none;
50
+ }
51
+
52
+ &:disabled {
53
+ cursor: not-allowed;
54
+ }
55
+ }
56
+
57
+ // Anchors - minimal reset (font only, let component classes control colors)
58
+ a {
59
+ font-family: inherit;
60
+ text-decoration: none;
61
+
62
+ &:hover {
63
+ text-decoration: none;
64
+ }
65
+ }
66
+
67
+ // Fieldset and Legend - remove default browser styling
68
+ fieldset {
69
+ border: 0;
70
+ padding: 0;
71
+ margin: 0;
72
+ }
73
+
74
+ legend {
75
+ font-family: inherit;
76
+ font-size: inherit;
77
+ padding: 0;
78
+ }
79
+
80
+ // Details and Summary - reset for consistent behavior
81
+ details {
82
+ font-family: inherit;
83
+ }
84
+
85
+ summary {
86
+ font-family: inherit;
87
+ cursor: pointer;
88
+ list-style: none;
89
+
90
+ &::-webkit-details-marker {
91
+ display: none;
92
+ }
93
+
94
+ &::marker {
95
+ display: none;
96
+ }
97
+ }
98
+
99
+ input,
100
+ select,
101
+ textarea {
102
+ font-family: $observ-font-family;
103
+ font-size: $observ-font-size-base;
104
+ line-height: 1.5;
105
+ color: $observ-text-primary;
106
+ background-color: $observ-bg-elevated;
107
+ border: 1px solid $observ-border-color;
108
+ border-radius: $observ-border-radius-sm;
109
+ margin: 0;
110
+
111
+ &:focus {
112
+ outline: none;
113
+ border-color: $observ-primary;
114
+ box-shadow: 0 0 0 3px rgba($observ-primary, 0.1);
115
+ }
116
+
117
+ // Placeholder styling - full reset with vendor prefixes
118
+ &::placeholder {
119
+ color: $observ-text-muted;
120
+ opacity: 1;
121
+ font-family: $observ-font-family;
122
+ font-style: normal;
123
+ font-weight: 400;
124
+ text-transform: none;
125
+ letter-spacing: normal;
126
+ }
127
+
128
+ &::-webkit-input-placeholder {
129
+ color: $observ-text-muted;
130
+ opacity: 1;
131
+ font-family: $observ-font-family;
132
+ font-style: normal;
133
+ font-weight: 400;
134
+ text-transform: none;
135
+ letter-spacing: normal;
136
+ }
137
+
138
+ &::-moz-placeholder {
139
+ color: $observ-text-muted;
140
+ opacity: 1;
141
+ font-family: $observ-font-family;
142
+ font-style: normal;
143
+ font-weight: 400;
144
+ text-transform: none;
145
+ letter-spacing: normal;
146
+ }
147
+
148
+ &:-ms-input-placeholder {
149
+ color: $observ-text-muted;
150
+ opacity: 1;
151
+ font-family: $observ-font-family;
152
+ font-style: normal;
153
+ font-weight: 400;
154
+ text-transform: none;
155
+ letter-spacing: normal;
156
+ }
157
+
158
+ &:disabled {
159
+ background-color: $observ-bg-surface;
160
+ color: $observ-text-muted;
161
+ cursor: not-allowed;
162
+ }
163
+ }
164
+
165
+ // Text inputs
166
+ input[type="text"],
167
+ input[type="email"],
168
+ input[type="password"],
169
+ input[type="number"],
170
+ input[type="search"],
171
+ input[type="tel"],
172
+ input[type="url"],
173
+ input[type="date"],
174
+ input[type="datetime-local"],
175
+ input[type="month"],
176
+ input[type="week"],
177
+ input[type="time"] {
178
+ height: auto;
179
+ padding: $observ-spacing-sm $observ-spacing-md;
180
+ background-color: $observ-bg-elevated;
181
+ color: $observ-text-primary;
182
+ }
183
+
184
+ // Date inputs - ensure consistent styling
185
+ input[type="date"],
186
+ input[type="datetime-local"],
187
+ input[type="month"],
188
+ input[type="week"],
189
+ input[type="time"] {
190
+ color-scheme: dark;
191
+
192
+ &::-webkit-calendar-picker-indicator {
193
+ cursor: pointer;
194
+ opacity: 0.6;
195
+ filter: invert(1);
196
+
197
+ &:hover {
198
+ opacity: 1;
199
+ }
200
+ }
201
+ }
202
+
203
+ // Select elements
204
+ select {
205
+ appearance: none;
206
+ -webkit-appearance: none;
207
+ -moz-appearance: none;
208
+ padding: $observ-spacing-sm $observ-spacing-md;
209
+ padding-right: calc(#{$observ-spacing-md} + 1.5rem);
210
+ background-color: $observ-bg-elevated;
211
+ color: $observ-text-primary;
212
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%239ca3af' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
213
+ background-repeat: no-repeat;
214
+ background-position: right $observ-spacing-sm center;
215
+ cursor: pointer;
216
+
217
+ &::-ms-expand {
218
+ display: none;
219
+ }
220
+
221
+ option {
222
+ background-color: $observ-bg-elevated;
223
+ color: $observ-text-primary;
224
+ }
225
+ }
226
+
227
+ // Textarea
228
+ textarea {
229
+ padding: $observ-spacing-sm $observ-spacing-md;
230
+ background-color: $observ-bg-elevated;
231
+ color: $observ-text-primary;
232
+ resize: vertical;
233
+ min-height: 80px;
234
+ }
235
+
236
+ // Checkbox and Radio - minimal reset
237
+ input[type="checkbox"],
238
+ input[type="radio"] {
239
+ width: auto;
240
+ height: auto;
241
+ margin: 0;
242
+ padding: 0;
243
+ cursor: pointer;
244
+ accent-color: $observ-primary;
245
+ // Don't override background for checkboxes/radios as they have native styling
246
+ background-color: transparent;
247
+ border: none;
248
+ }
249
+ }
@@ -1,53 +1,61 @@
1
1
  @import 'variables';
2
+ @import 'namespace';
2
3
 
3
- .observ-table {
4
- width: 100%;
5
- border-collapse: collapse;
6
- font-size: $observ-font-size-sm;
7
-
8
- &__header {
9
- background-color: $observ-gray-50;
4
+ @include observ-scoped {
5
+ .observ-table-container {
6
+ overflow-x: auto;
10
7
  }
11
8
 
12
- &__row {
13
- border-bottom: 1px solid $observ-gray-200;
14
- transition: $observ-transition;
9
+ .observ-table {
10
+ width: 100%;
11
+ border-collapse: collapse;
12
+ font-size: $observ-font-size-sm;
13
+ background-color: $observ-bg-surface;
15
14
 
16
- &:hover {
17
- background-color: $observ-gray-50;
15
+ &__header {
16
+ background-color: $observ-bg-elevated;
18
17
  }
19
- }
20
18
 
21
- &__cell {
22
- padding: $observ-spacing-md;
23
- text-align: left;
24
- color: $observ-gray-700;
19
+ &__row {
20
+ border-bottom: 1px solid $observ-border-subtle;
21
+ transition: $observ-transition;
25
22
 
26
- &--numeric {
27
- text-align: right;
28
- font-variant-numeric: tabular-nums;
23
+ &:hover {
24
+ background-color: $observ-bg-hover;
25
+ }
29
26
  }
30
27
 
31
- &--actions {
32
- text-align: right;
33
- width: 1%;
34
- white-space: nowrap;
28
+ &__cell {
29
+ padding: $observ-spacing-md;
30
+ text-align: left;
31
+ color: $observ-text-secondary;
32
+ vertical-align: middle;
33
+
34
+ &--numeric {
35
+ text-align: right;
36
+ font-variant-numeric: tabular-nums;
37
+ }
38
+
39
+ &--actions {
40
+ text-align: right;
41
+ width: 1%;
42
+ white-space: nowrap;
43
+ }
44
+
45
+ thead & {
46
+ font-weight: 600;
47
+ color: $observ-text-primary;
48
+ font-size: $observ-font-size-sm;
49
+ padding-top: $observ-spacing-sm;
50
+ padding-bottom: $observ-spacing-sm;
51
+ background-color: $observ-bg-elevated;
52
+ }
35
53
  }
36
54
 
37
- thead & {
38
- font-weight: 600;
39
- color: $observ-gray-900;
40
- text-transform: uppercase;
41
- letter-spacing: 0.05em;
42
- font-size: $observ-font-size-xs;
43
- padding-top: $observ-spacing-sm;
44
- padding-bottom: $observ-spacing-sm;
45
- }
46
- }
47
-
48
- &--compact {
49
- .observ-table__cell {
50
- padding: $observ-spacing-sm $observ-spacing-md;
55
+ &--compact {
56
+ .observ-table__cell {
57
+ padding: $observ-spacing-sm $observ-spacing-md;
58
+ }
51
59
  }
52
60
  }
53
61
  }
@@ -1,9 +1,16 @@
1
+ // =============================================================================
2
+ // BRAND & ACCENT COLORS
3
+ // =============================================================================
1
4
  $observ-primary: #3b82f6;
2
5
  $observ-success: #10b981;
3
6
  $observ-warning: #f59e0b;
4
7
  $observ-danger: #ef4444;
5
8
  $observ-info: #06b6d4;
9
+ $observ-purple: #a78bfa; // AI/special features accent
6
10
 
11
+ // =============================================================================
12
+ // GRAY SCALE
13
+ // =============================================================================
7
14
  $observ-gray-50: #f9fafb;
8
15
  $observ-gray-100: #f3f4f6;
9
16
  $observ-gray-200: #e5e7eb;
@@ -18,6 +25,53 @@ $observ-gray-900: #111827;
18
25
  $observ-white: #ffffff;
19
26
  $observ-black: #000000;
20
27
 
28
+ // =============================================================================
29
+ // DARK THEME - Base Colors (Better Stack inspired)
30
+ // =============================================================================
31
+ $observ-dark-bg-page: #0d0d1a; // Deep navy page background
32
+ $observ-dark-bg-surface: #1a1a2e; // Card/surface background
33
+ $observ-dark-bg-elevated: #252540; // Elevated elements (dropdowns, modals)
34
+ $observ-dark-bg-hover: #2a2a45; // Hover states
35
+
36
+ // =============================================================================
37
+ // DARK THEME - Borders
38
+ // =============================================================================
39
+ $observ-dark-border: #2a2a40; // Standard border
40
+ $observ-dark-border-subtle: #1f1f35; // Subtle dividers
41
+ $observ-dark-border-strong: #3a3a55; // Emphasized borders
42
+
43
+ // =============================================================================
44
+ // DARK THEME - Text
45
+ // =============================================================================
46
+ $observ-dark-text-primary: #f3f4f6; // Primary text (gray-100)
47
+ $observ-dark-text-secondary: #9ca3af; // Secondary text (gray-400)
48
+ $observ-dark-text-muted: #6b7280; // Muted text (gray-500)
49
+
50
+ // =============================================================================
51
+ // SEMANTIC COLOR MAPPINGS (dark theme as default)
52
+ // =============================================================================
53
+ $observ-bg-page: $observ-dark-bg-page;
54
+ $observ-bg-surface: $observ-dark-bg-surface;
55
+ $observ-bg-elevated: $observ-dark-bg-elevated;
56
+ $observ-bg-hover: $observ-dark-bg-hover;
57
+
58
+ $observ-border-color: $observ-dark-border;
59
+ $observ-border-subtle: $observ-dark-border-subtle;
60
+ $observ-border-strong: $observ-dark-border-strong;
61
+
62
+ $observ-text-primary: $observ-dark-text-primary;
63
+ $observ-text-secondary: $observ-dark-text-secondary;
64
+ $observ-text-muted: $observ-dark-text-muted;
65
+
66
+ // =============================================================================
67
+ // SYNTAX HIGHLIGHTING (for JSON viewer, code blocks)
68
+ // =============================================================================
69
+ $observ-syntax-number: #ff9800; // Orange for numbers
70
+ $observ-syntax-boolean: #c792ea; // Purple for booleans
71
+
72
+ // =============================================================================
73
+ // SPACING
74
+ // =============================================================================
21
75
  $observ-spacing-xs: 0.25rem;
22
76
  $observ-spacing-sm: 0.5rem;
23
77
  $observ-spacing-md: 1rem;
@@ -1,4 +1,6 @@
1
1
  @import 'variables';
2
+ @import 'namespace';
3
+ @import 'reset';
2
4
  @import 'layout';
3
5
  @import 'card';
4
6
  @import 'metrics';
@@ -11,5 +13,6 @@
11
13
  @import 'annotations';
12
14
  @import 'prompts';
13
15
  @import 'datasets';
16
+ @import 'filters';
14
17
  @import 'json_viewer';
15
18
  @import 'pagination';
@@ -19,7 +19,6 @@ module Observ
19
19
 
20
20
  score = @run_item.scores.find_or_initialize_by(name: "manual", source: :manual)
21
21
  score.assign_attributes(
22
- trace: @run_item.trace,
23
22
  value: value,
24
23
  data_type: :boolean,
25
24
  comment: params[:comment],
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Observ
4
+ class ReviewQueueController < ApplicationController
5
+ before_action :set_review_item, only: [ :show, :complete, :skip ]
6
+
7
+ def index
8
+ @review_items = Observ::ReviewItem
9
+ .actionable
10
+ .by_priority
11
+ .includes(:reviewable)
12
+ .page(params[:page])
13
+ .per(Observ.config.pagination_per_page)
14
+
15
+ @stats = queue_stats
16
+ @filter = :all
17
+ end
18
+
19
+ def sessions
20
+ @review_items = Observ::ReviewItem
21
+ .sessions
22
+ .actionable
23
+ .by_priority
24
+ .includes(:reviewable)
25
+ .page(params[:page])
26
+ .per(Observ.config.pagination_per_page)
27
+
28
+ @stats = queue_stats(scope: :sessions)
29
+ @filter = :sessions
30
+ render :index
31
+ end
32
+
33
+ def traces
34
+ @review_items = Observ::ReviewItem
35
+ .traces
36
+ .actionable
37
+ .by_priority
38
+ .includes(:reviewable)
39
+ .page(params[:page])
40
+ .per(Observ.config.pagination_per_page)
41
+
42
+ @stats = queue_stats(scope: :traces)
43
+ @filter = :traces
44
+ render :index
45
+ end
46
+
47
+ def show
48
+ @review_item.start_review!
49
+ @reviewable = @review_item.reviewable
50
+ @next_item = next_review_item
51
+ @queue_position = queue_position
52
+
53
+ # Load additional data for sessions
54
+ if @reviewable.is_a?(Observ::Session)
55
+ @session_metrics = @reviewable.session_metrics
56
+ @traces = @reviewable.traces.includes(:observations).order(start_time: :asc).limit(10)
57
+ @chat = @reviewable.chat if defined?(::Chat)
58
+ elsif @reviewable.is_a?(Observ::Trace)
59
+ @observations = @reviewable.observations.order(start_time: :asc).limit(5)
60
+ end
61
+ end
62
+
63
+ def complete
64
+ @review_item.complete!(by: params[:completed_by])
65
+
66
+ if params[:next] && (next_item = next_review_item)
67
+ redirect_to review_path(next_item), notice: "Review saved. Showing next item."
68
+ else
69
+ redirect_to reviews_path, notice: "Review completed."
70
+ end
71
+ end
72
+
73
+ def skip
74
+ @review_item.skip!(by: params[:completed_by])
75
+
76
+ if (next_item = next_review_item)
77
+ redirect_to review_path(next_item), notice: "Skipped. Showing next item."
78
+ else
79
+ redirect_to reviews_path, notice: "Item skipped. No more items to review."
80
+ end
81
+ end
82
+
83
+ def stats
84
+ @stats = detailed_stats
85
+ end
86
+
87
+ private
88
+
89
+ def set_review_item
90
+ @review_item = Observ::ReviewItem.find(params[:id])
91
+ end
92
+
93
+ def next_review_item
94
+ Observ::ReviewItem
95
+ .actionable
96
+ .by_priority
97
+ .where.not(id: @review_item&.id)
98
+ .first
99
+ end
100
+
101
+ def queue_position
102
+ return nil unless @review_item
103
+
104
+ # Convert enum string to integer for SQL comparison
105
+ priority_value = Observ::ReviewItem.priorities[@review_item.priority]
106
+
107
+ {
108
+ current: Observ::ReviewItem.actionable.where(
109
+ "priority > :priority OR (priority = :priority AND created_at < :created_at)",
110
+ priority: priority_value,
111
+ created_at: @review_item.created_at
112
+ ).count + 1,
113
+ total: Observ::ReviewItem.actionable.count
114
+ }
115
+ end
116
+
117
+ def queue_stats(scope: nil)
118
+ base = Observ::ReviewItem
119
+ base = base.send(scope) if scope
120
+
121
+ {
122
+ pending: base.pending.count,
123
+ in_progress: base.in_progress.count,
124
+ completed_today: base.completed.where("completed_at >= ?", Time.current.beginning_of_day).count,
125
+ completed_this_week: base.completed.where("completed_at >= ?", Time.current.beginning_of_week).count
126
+ }
127
+ end
128
+
129
+ def detailed_stats
130
+ completed = Observ::ReviewItem.completed
131
+
132
+ {
133
+ total_pending: Observ::ReviewItem.actionable.count,
134
+ total_completed: completed.count,
135
+ completed_today: completed.where("completed_at >= ?", Time.current.beginning_of_day).count,
136
+ completed_this_week: completed.where("completed_at >= ?", Time.current.beginning_of_week).count,
137
+ by_reason: Observ::ReviewItem.group(:reason).count,
138
+ by_priority: Observ::ReviewItem.group(:priority).count,
139
+ pass_rate: calculate_pass_rate
140
+ }
141
+ end
142
+
143
+ def calculate_pass_rate
144
+ completed_items = Observ::ReviewItem.completed.includes(:reviewable)
145
+ return nil if completed_items.empty?
146
+
147
+ passed = completed_items.count do |item|
148
+ item.reviewable&.manual_score&.passed?
149
+ end
150
+
151
+ (passed.to_f / completed_items.count * 100).round(1)
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Observ
4
+ class ScoresController < ApplicationController
5
+ before_action :set_scoreable
6
+
7
+ def create
8
+ value = parse_score_value(params[:value], params[:data_type])
9
+
10
+ @score = @scoreable.scores.find_or_initialize_by(
11
+ name: params[:name] || "manual",
12
+ source: :manual
13
+ )
14
+
15
+ @score.assign_attributes(
16
+ value: value,
17
+ data_type: params[:data_type] || :boolean,
18
+ comment: params[:comment],
19
+ created_by: params[:created_by]
20
+ )
21
+
22
+ if @score.save
23
+ respond_to do |format|
24
+ format.turbo_stream
25
+ format.html { redirect_back(fallback_location: root_path, notice: "Score saved.") }
26
+ end
27
+ else
28
+ respond_to do |format|
29
+ format.turbo_stream { render :create_error, status: :unprocessable_entity }
30
+ format.html { redirect_back(fallback_location: root_path, alert: "Failed to save score.") }
31
+ end
32
+ end
33
+ end
34
+
35
+ def destroy
36
+ @score = @scoreable.scores.find(params[:id])
37
+ @score.destroy
38
+
39
+ respond_to do |format|
40
+ format.turbo_stream { render turbo_stream: turbo_stream.remove("score_#{@score.id}") }
41
+ format.html { redirect_back(fallback_location: root_path, notice: "Score deleted.") }
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def set_scoreable
48
+ if params[:session_id]
49
+ @scoreable = Observ::Session.find(params[:session_id])
50
+ elsif params[:trace_id]
51
+ @scoreable = Observ::Trace.find(params[:trace_id])
52
+ end
53
+ end
54
+
55
+ def parse_score_value(value, data_type)
56
+ case data_type&.to_sym
57
+ when :boolean
58
+ value.to_i == 1 ? 1.0 : 0.0
59
+ else
60
+ value.to_f
61
+ end
62
+ end
63
+ end
64
+ end
@@ -2,6 +2,7 @@ module Observ
2
2
  class SessionsController < ApplicationController
3
3
  def index
4
4
  @sessions = Observ::Session.order(start_time: :desc)
5
+ @agent_types = distinct_agent_types
5
6
  apply_filters if params[:filter].present?
6
7
  @sessions = @sessions.page(params[:page]).per(Observ.config.pagination_per_page)
7
8
  end
@@ -30,6 +31,15 @@ module Observ
30
31
 
31
32
  private
32
33
 
34
+ def distinct_agent_types
35
+ Observ::Session
36
+ .where.not(metadata: nil)
37
+ .pluck(:metadata)
38
+ .filter_map { |m| m&.dig("agent_type") }
39
+ .uniq
40
+ .sort
41
+ end
42
+
33
43
  def apply_filters
34
44
  @sessions = @sessions.where(user_id: params[:filter][:user_id]) if params[:filter][:user_id].present?
35
45
 
@@ -40,6 +50,19 @@ module Observ
40
50
  if params[:filter][:end_date].present?
41
51
  @sessions = @sessions.where("start_time <= ?", params[:filter][:end_date])
42
52
  end
53
+
54
+ if params[:filter][:status].present?
55
+ case params[:filter][:status]
56
+ when "completed"
57
+ @sessions = @sessions.where.not(end_time: nil)
58
+ when "in_progress"
59
+ @sessions = @sessions.where(end_time: nil)
60
+ end
61
+ end
62
+
63
+ if params[:filter][:agent_type].present?
64
+ @sessions = @sessions.where_json(:metadata, :agent_type, params[:filter][:agent_type])
65
+ end
43
66
  end
44
67
  end
45
68
  end