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.
- checksums.yaml +7 -0
- data/README.md +778 -0
- data/Rakefile +49 -0
- data/app/assets/javascripts/observ/application.js +12 -0
- data/app/assets/javascripts/observ/controllers/autoscroll_controller.js +33 -0
- data/app/assets/javascripts/observ/controllers/chat_form_controller.js +93 -0
- data/app/assets/javascripts/observ/controllers/copy_controller.js +43 -0
- data/app/assets/javascripts/observ/controllers/dashboard_controller.js +58 -0
- data/app/assets/javascripts/observ/controllers/drawer_controller.js +58 -0
- data/app/assets/javascripts/observ/controllers/expandable_controller.js +33 -0
- data/app/assets/javascripts/observ/controllers/filter_controller.js +36 -0
- data/app/assets/javascripts/observ/controllers/index.js +52 -0
- data/app/assets/javascripts/observ/controllers/json_viewer_controller.js +260 -0
- data/app/assets/javascripts/observ/controllers/message_form_controller.js +58 -0
- data/app/assets/javascripts/observ/controllers/prompt_variables_controller.js +64 -0
- data/app/assets/javascripts/observ/controllers/text_select_controller.js +14 -0
- data/app/assets/stylesheets/observ/_annotations.scss +127 -0
- data/app/assets/stylesheets/observ/_card.scss +52 -0
- data/app/assets/stylesheets/observ/_chat.scss +156 -0
- data/app/assets/stylesheets/observ/_components.scss +460 -0
- data/app/assets/stylesheets/observ/_dashboard.scss +40 -0
- data/app/assets/stylesheets/observ/_datasets.scss +697 -0
- data/app/assets/stylesheets/observ/_drawer.scss +273 -0
- data/app/assets/stylesheets/observ/_json_viewer.scss +120 -0
- data/app/assets/stylesheets/observ/_layout.scss +256 -0
- data/app/assets/stylesheets/observ/_metrics.scss +99 -0
- data/app/assets/stylesheets/observ/_observations.scss +160 -0
- data/app/assets/stylesheets/observ/_pagination.scss +143 -0
- data/app/assets/stylesheets/observ/_prompts.scss +365 -0
- data/app/assets/stylesheets/observ/_table.scss +53 -0
- data/app/assets/stylesheets/observ/_variables.scss +53 -0
- data/app/assets/stylesheets/observ/application.scss +15 -0
- data/app/controllers/observ/annotations_controller.rb +144 -0
- data/app/controllers/observ/application_controller.rb +8 -0
- data/app/controllers/observ/chats_controller.rb +58 -0
- data/app/controllers/observ/dashboard_controller.rb +159 -0
- data/app/controllers/observ/dataset_items_controller.rb +85 -0
- data/app/controllers/observ/dataset_run_items_controller.rb +84 -0
- data/app/controllers/observ/dataset_runs_controller.rb +110 -0
- data/app/controllers/observ/datasets_controller.rb +74 -0
- data/app/controllers/observ/messages_controller.rb +26 -0
- data/app/controllers/observ/observations_controller.rb +59 -0
- data/app/controllers/observ/prompt_versions_controller.rb +148 -0
- data/app/controllers/observ/prompts_controller.rb +205 -0
- data/app/controllers/observ/sessions_controller.rb +45 -0
- data/app/controllers/observ/traces_controller.rb +86 -0
- data/app/forms/observ/prompt_form.rb +96 -0
- data/app/helpers/observ/application_helper.rb +9 -0
- data/app/helpers/observ/chats_helper.rb +47 -0
- data/app/helpers/observ/dashboard_helper.rb +154 -0
- data/app/helpers/observ/datasets_helper.rb +62 -0
- data/app/helpers/observ/pagination_helper.rb +38 -0
- data/app/jobs/observ/application_job.rb +4 -0
- data/app/jobs/observ/dataset_runner_job.rb +49 -0
- data/app/mailers/observ/application_mailer.rb +6 -0
- data/app/models/concerns/observ/agent_phaseable.rb +124 -0
- data/app/models/concerns/observ/agent_selectable.rb +50 -0
- data/app/models/concerns/observ/chat_enhancements.rb +109 -0
- data/app/models/concerns/observ/message_enhancements.rb +31 -0
- data/app/models/concerns/observ/observability_instrumentation.rb +124 -0
- data/app/models/concerns/observ/prompt_management.rb +320 -0
- data/app/models/concerns/observ/trace_association.rb +9 -0
- data/app/models/observ/annotation.rb +23 -0
- data/app/models/observ/application_record.rb +5 -0
- data/app/models/observ/dataset.rb +51 -0
- data/app/models/observ/dataset_item.rb +41 -0
- data/app/models/observ/dataset_run.rb +104 -0
- data/app/models/observ/dataset_run_item.rb +111 -0
- data/app/models/observ/generation.rb +56 -0
- data/app/models/observ/null_prompt.rb +59 -0
- data/app/models/observ/observation.rb +38 -0
- data/app/models/observ/prompt.rb +315 -0
- data/app/models/observ/score.rb +51 -0
- data/app/models/observ/session.rb +131 -0
- data/app/models/observ/span.rb +13 -0
- data/app/models/observ/trace.rb +135 -0
- data/app/presenters/observ/agent_select_presenter.rb +59 -0
- data/app/services/observ/agent_executor_service.rb +174 -0
- data/app/services/observ/agent_provider.rb +60 -0
- data/app/services/observ/agent_selection_service.rb +53 -0
- data/app/services/observ/chat_instrumenter.rb +523 -0
- data/app/services/observ/dataset_runner_service.rb +153 -0
- data/app/services/observ/evaluator_runner_service.rb +58 -0
- data/app/services/observ/evaluators/base_evaluator.rb +51 -0
- data/app/services/observ/evaluators/contains_evaluator.rb +53 -0
- data/app/services/observ/evaluators/exact_match_evaluator.rb +23 -0
- data/app/services/observ/evaluators/json_structure_evaluator.rb +44 -0
- data/app/services/observ/prompt_manager/cache_statistics.rb +82 -0
- data/app/services/observ/prompt_manager/caching.rb +167 -0
- data/app/services/observ/prompt_manager/comparison.rb +49 -0
- data/app/services/observ/prompt_manager/version_management.rb +96 -0
- data/app/services/observ/prompt_manager.rb +40 -0
- data/app/services/observ/trace_text_formatter.rb +349 -0
- data/app/validators/observ/prompt_config_validator.rb +187 -0
- data/app/views/kaminari/_first_page.html.erb +11 -0
- data/app/views/kaminari/_gap.html.erb +8 -0
- data/app/views/kaminari/_last_page.html.erb +11 -0
- data/app/views/kaminari/_next_page.html.erb +11 -0
- data/app/views/kaminari/_page.html.erb +12 -0
- data/app/views/kaminari/_paginator.html.erb +25 -0
- data/app/views/kaminari/_prev_page.html.erb +11 -0
- data/app/views/kaminari/observ/_first_page.html.erb +11 -0
- data/app/views/kaminari/observ/_gap.html.erb +8 -0
- data/app/views/kaminari/observ/_last_page.html.erb +11 -0
- data/app/views/kaminari/observ/_next_page.html.erb +11 -0
- data/app/views/kaminari/observ/_page.html.erb +12 -0
- data/app/views/kaminari/observ/_paginator.html.erb +25 -0
- data/app/views/kaminari/observ/_prev_page.html.erb +11 -0
- data/app/views/layouts/observ/application.html.erb +88 -0
- data/app/views/observ/annotations/_annotation.html.erb +13 -0
- data/app/views/observ/annotations/_form.html.erb +28 -0
- data/app/views/observ/annotations/index.html.erb +28 -0
- data/app/views/observ/annotations/sessions_index.html.erb +48 -0
- data/app/views/observ/annotations/traces_index.html.erb +48 -0
- data/app/views/observ/chats/_form.html.erb +45 -0
- data/app/views/observ/chats/index.html.erb +67 -0
- data/app/views/observ/chats/new.html.erb +17 -0
- data/app/views/observ/chats/show.html.erb +34 -0
- data/app/views/observ/dashboard/index.html.erb +236 -0
- data/app/views/observ/dataset_items/_form.html.erb +49 -0
- data/app/views/observ/dataset_items/edit.html.erb +18 -0
- data/app/views/observ/dataset_items/index.html.erb +95 -0
- data/app/views/observ/dataset_items/new.html.erb +18 -0
- data/app/views/observ/dataset_run_items/_score_close_drawer.html.erb +4 -0
- data/app/views/observ/dataset_run_items/_score_drawer.html.erb +75 -0
- data/app/views/observ/dataset_run_items/_score_success.html.erb +29 -0
- data/app/views/observ/dataset_run_items/_scores_cell.html.erb +19 -0
- data/app/views/observ/dataset_run_items/details_drawer.turbo_stream.erb +80 -0
- data/app/views/observ/dataset_run_items/score_drawer.turbo_stream.erb +7 -0
- data/app/views/observ/dataset_runs/index.html.erb +108 -0
- data/app/views/observ/dataset_runs/new.html.erb +57 -0
- data/app/views/observ/dataset_runs/review.html.erb +155 -0
- data/app/views/observ/dataset_runs/show.html.erb +166 -0
- data/app/views/observ/datasets/_form.html.erb +62 -0
- data/app/views/observ/datasets/_items_tab.html.erb +66 -0
- data/app/views/observ/datasets/_runs_tab.html.erb +82 -0
- data/app/views/observ/datasets/edit.html.erb +32 -0
- data/app/views/observ/datasets/index.html.erb +105 -0
- data/app/views/observ/datasets/new.html.erb +18 -0
- data/app/views/observ/datasets/show.html.erb +67 -0
- data/app/views/observ/messages/_content.html.erb +1 -0
- data/app/views/observ/messages/_form.html.erb +33 -0
- data/app/views/observ/messages/_message.html.erb +14 -0
- data/app/views/observ/messages/_tool_calls.html.erb +10 -0
- data/app/views/observ/messages/create.turbo_stream.erb +9 -0
- data/app/views/observ/observations/index.html.erb +97 -0
- data/app/views/observ/observations/show_generation.html.erb +195 -0
- data/app/views/observ/observations/show_span.html.erb +93 -0
- data/app/views/observ/prompts/_diff_content.html.erb +16 -0
- data/app/views/observ/prompts/_form.html.erb +111 -0
- data/app/views/observ/prompts/_new_form.html.erb +102 -0
- data/app/views/observ/prompts/_prompt_actions.html.erb +4 -0
- data/app/views/observ/prompts/_prompt_content_highlighted.html.erb +4 -0
- data/app/views/observ/prompts/_version_actions.html.erb +40 -0
- data/app/views/observ/prompts/compare.html.erb +155 -0
- data/app/views/observ/prompts/edit.html.erb +17 -0
- data/app/views/observ/prompts/index.html.erb +108 -0
- data/app/views/observ/prompts/new.html.erb +17 -0
- data/app/views/observ/prompts/show.html.erb +138 -0
- data/app/views/observ/prompts/versions.html.erb +87 -0
- data/app/views/observ/sessions/annotations_drawer.turbo_stream.erb +25 -0
- data/app/views/observ/sessions/drawer_test.turbo_stream.erb +49 -0
- data/app/views/observ/sessions/index.html.erb +91 -0
- data/app/views/observ/sessions/show.html.erb +251 -0
- data/app/views/observ/traces/add_to_dataset_drawer.turbo_stream.erb +48 -0
- data/app/views/observ/traces/annotations_drawer.turbo_stream.erb +25 -0
- data/app/views/observ/traces/index.html.erb +87 -0
- data/app/views/observ/traces/show.html.erb +285 -0
- data/app/views/observ/traces/text_output_drawer.turbo_stream.erb +48 -0
- data/app/views/shared/_drawer.html.erb +26 -0
- data/config/routes.rb +80 -0
- data/db/migrate/001_create_observ_sessions.rb +21 -0
- data/db/migrate/002_create_observ_traces.rb +25 -0
- data/db/migrate/003_create_observ_observations.rb +42 -0
- data/db/migrate/004_add_message_id_to_observ_traces.rb +7 -0
- data/db/migrate/005_create_observ_prompts.rb +21 -0
- data/db/migrate/006_fix_prompt_config_strings.rb +23 -0
- data/db/migrate/007_create_observ_annotations.rb +12 -0
- data/db/migrate/009_add_prompt_fields_to_observ_chats.rb +11 -0
- data/db/migrate/010_create_observ_datasets.rb +15 -0
- data/db/migrate/011_create_observ_dataset_items.rb +17 -0
- data/db/migrate/012_create_observ_dataset_runs.rb +22 -0
- data/db/migrate/013_create_observ_dataset_run_items.rb +16 -0
- data/db/migrate/014_create_observ_scores.rb +26 -0
- data/lib/generators/observ/add_phase_tracking/add_phase_tracking_generator.rb +150 -0
- data/lib/generators/observ/add_phase_tracking/templates/migration.rb.tt +6 -0
- data/lib/generators/observ/install/USAGE +27 -0
- data/lib/generators/observ/install/install_generator.rb +270 -0
- data/lib/generators/observ/install_chat/install_chat_generator.rb +313 -0
- data/lib/generators/observ/install_chat/templates/agents/base_agent.rb.tt +147 -0
- data/lib/generators/observ/install_chat/templates/agents/simple_agent.rb.tt +55 -0
- data/lib/generators/observ/install_chat/templates/concerns/observ_chat_enhancements.rb.tt +34 -0
- data/lib/generators/observ/install_chat/templates/concerns/observ_message_enhancements.rb.tt +18 -0
- data/lib/generators/observ/install_chat/templates/initializers/observability.rb.tt +20 -0
- data/lib/generators/observ/install_chat/templates/jobs/chat_response_job.rb.tt +56 -0
- data/lib/generators/observ/install_chat/templates/migrations/add_agent_class_name.rb.tt +6 -0
- data/lib/generators/observ/install_chat/templates/migrations/add_observability_session_id.rb.tt +6 -0
- data/lib/generators/observ/install_chat/templates/tools/think_tool.rb.tt +29 -0
- data/lib/generators/observ/install_chat/templates/views/messages/_content.html.erb.tt +1 -0
- data/lib/observ/asset_installer.rb +130 -0
- data/lib/observ/asset_syncer.rb +104 -0
- data/lib/observ/configuration.rb +108 -0
- data/lib/observ/engine.rb +50 -0
- data/lib/observ/index_file_generator.rb +142 -0
- data/lib/observ/instrumenter/ruby_llm.rb +6 -0
- data/lib/observ/version.rb +3 -0
- data/lib/observ.rb +29 -0
- data/lib/tasks/observ_tasks.rake +75 -0
- 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,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,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
|