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,349 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Observ
|
|
4
|
+
class TraceTextFormatter
|
|
5
|
+
SEPARATOR = "=" * 80
|
|
6
|
+
SUBSEPARATOR = "-" * 80
|
|
7
|
+
INDENT = " "
|
|
8
|
+
MAX_CONTENT_LENGTH = 10_000
|
|
9
|
+
|
|
10
|
+
attr_reader :trace
|
|
11
|
+
|
|
12
|
+
def initialize(trace)
|
|
13
|
+
@trace = trace
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Main public method to format the trace as plain text
|
|
17
|
+
def format
|
|
18
|
+
parts = []
|
|
19
|
+
parts << format_trace_header
|
|
20
|
+
parts << format_trace_details
|
|
21
|
+
parts << format_trace_annotations if trace.annotations.any?
|
|
22
|
+
parts << ""
|
|
23
|
+
parts << format_observations_section
|
|
24
|
+
parts << SEPARATOR
|
|
25
|
+
parts.join("\n")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def format_trace_header
|
|
31
|
+
[
|
|
32
|
+
SEPARATOR,
|
|
33
|
+
"TRACE: #{trace.name || 'Unnamed Trace'}",
|
|
34
|
+
SEPARATOR
|
|
35
|
+
].join("\n")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def format_trace_details
|
|
39
|
+
details = []
|
|
40
|
+
details << "Trace ID: #{trace.trace_id}"
|
|
41
|
+
details << "Start Time: #{format_time(trace.start_time)}"
|
|
42
|
+
details << "End Time: #{format_time(trace.end_time)}" if trace.end_time
|
|
43
|
+
details << "Duration: #{format_duration(trace.duration_ms)}" if trace.duration_ms
|
|
44
|
+
details << "Total Cost: #{format_cost(trace.total_cost)}" if trace.total_cost && trace.total_cost > 0
|
|
45
|
+
details << "Total Tokens: #{trace.total_tokens}" if trace.total_tokens && trace.total_tokens > 0
|
|
46
|
+
|
|
47
|
+
if trace.user_id.present?
|
|
48
|
+
details << "User ID: #{trace.user_id}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if trace.release.present?
|
|
52
|
+
details << "Release: #{trace.release}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if trace.version.present?
|
|
56
|
+
details << "Version: #{trace.version}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if trace.input.present?
|
|
60
|
+
details << ""
|
|
61
|
+
details << "Input:"
|
|
62
|
+
details << format_content(trace.input)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if trace.output.present?
|
|
66
|
+
details << ""
|
|
67
|
+
details << "Output:"
|
|
68
|
+
details << format_content(trace.output)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if trace.metadata.present? && trace.metadata.any?
|
|
72
|
+
details << ""
|
|
73
|
+
details << "Metadata:"
|
|
74
|
+
details << format_json(trace.metadata)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if trace.tags.present? && trace.tags.any?
|
|
78
|
+
details << ""
|
|
79
|
+
details << "Tags: #{trace.tags.to_json}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
details.join("\n")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def format_trace_annotations
|
|
86
|
+
annotations = trace.annotations.order(created_at: :asc)
|
|
87
|
+
return "" if annotations.empty?
|
|
88
|
+
|
|
89
|
+
parts = []
|
|
90
|
+
parts << ""
|
|
91
|
+
parts << "--- ANNOTATIONS (#{annotations.count}) ---"
|
|
92
|
+
|
|
93
|
+
annotations.each do |annotation|
|
|
94
|
+
parts << format_annotation(annotation)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
parts.join("\n")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def format_observations_section
|
|
101
|
+
observations = trace.observations.order(start_time: :asc)
|
|
102
|
+
return "No observations recorded." if observations.empty?
|
|
103
|
+
|
|
104
|
+
parts = []
|
|
105
|
+
parts << SEPARATOR
|
|
106
|
+
parts << "OBSERVATIONS (#{observations.count})"
|
|
107
|
+
parts << SEPARATOR
|
|
108
|
+
parts << ""
|
|
109
|
+
|
|
110
|
+
# Build tree structure
|
|
111
|
+
observations_by_parent = observations.group_by(&:parent_observation_id)
|
|
112
|
+
root_observations = observations_by_parent[nil] || []
|
|
113
|
+
|
|
114
|
+
root_observations.each do |observation|
|
|
115
|
+
parts << format_observation(observation, observations_by_parent, 0)
|
|
116
|
+
parts << ""
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
parts.join("\n")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def format_observation(observation, observations_by_parent, depth)
|
|
123
|
+
indent = INDENT * depth
|
|
124
|
+
parts = []
|
|
125
|
+
|
|
126
|
+
# Observation header
|
|
127
|
+
parts << "#{indent}┌─ [#{observation.type.demodulize.upcase}] #{observation.name}"
|
|
128
|
+
parts << "#{indent}│ Observation ID: #{observation.observation_id}"
|
|
129
|
+
parts << "#{indent}│ Start: #{format_time(observation.start_time)}"
|
|
130
|
+
parts << "#{indent}│ End: #{format_time(observation.end_time)}" if observation.end_time
|
|
131
|
+
parts << "#{indent}│ Duration: #{format_duration(observation.duration_ms)}" if observation.duration_ms
|
|
132
|
+
|
|
133
|
+
# Type-specific formatting
|
|
134
|
+
if observation.is_a?(Observ::Generation)
|
|
135
|
+
parts << format_generation_details(observation, indent)
|
|
136
|
+
elsif observation.is_a?(Observ::Span)
|
|
137
|
+
parts << format_span_details(observation, indent)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
parts << "#{indent}└─ End of #{observation.type.demodulize.upcase}"
|
|
141
|
+
|
|
142
|
+
# Recursively format child observations
|
|
143
|
+
children = observations_by_parent[observation.observation_id] || []
|
|
144
|
+
if children.any?
|
|
145
|
+
parts << ""
|
|
146
|
+
parts << "#{indent} ┌─ CHILD OBSERVATIONS (#{children.count})"
|
|
147
|
+
children.each do |child|
|
|
148
|
+
parts << format_observation(child, observations_by_parent, depth + 1)
|
|
149
|
+
end
|
|
150
|
+
parts << "#{indent} └─ END CHILD OBSERVATIONS"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
parts.join("\n")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def format_generation_details(generation, indent)
|
|
157
|
+
details = []
|
|
158
|
+
|
|
159
|
+
if generation.model.present?
|
|
160
|
+
details << "#{indent}│ Model: #{generation.model}"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if generation.cost_usd && generation.cost_usd > 0
|
|
164
|
+
details << "#{indent}│ Cost: #{format_cost(generation.cost_usd)}"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
if generation.usage.present? && generation.usage.any?
|
|
168
|
+
details << "#{indent}│ "
|
|
169
|
+
details << "#{indent}│ Usage:"
|
|
170
|
+
generation.usage.each do |key, value|
|
|
171
|
+
details << "#{indent}│ - #{key.to_s.humanize}: #{value}"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if generation.model_parameters.present? && generation.model_parameters.any?
|
|
176
|
+
details << "#{indent}│ "
|
|
177
|
+
details << "#{indent}│ Model Parameters:"
|
|
178
|
+
generation.model_parameters.each do |key, value|
|
|
179
|
+
details << "#{indent}│ - #{key}: #{value}"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
if generation.prompt_name.present?
|
|
184
|
+
details << "#{indent}│ Prompt Name: #{generation.prompt_name}"
|
|
185
|
+
details << "#{indent}│ Prompt Version: #{generation.prompt_version}" if generation.prompt_version.present?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
if generation.finish_reason.present?
|
|
189
|
+
details << "#{indent}│ Finish Reason: #{generation.finish_reason}"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
if generation.status_message.present?
|
|
193
|
+
details << "#{indent}│ Status: #{generation.status_message}"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if generation.messages.present? && generation.messages.any?
|
|
197
|
+
details << "#{indent}│ "
|
|
198
|
+
details << "#{indent}│ Messages:"
|
|
199
|
+
generation.messages.each_with_index do |msg, idx|
|
|
200
|
+
details << "#{indent}│ [#{idx + 1}] #{msg['role']}: #{truncate_content(msg['content'], 200)}"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
if generation.tools.present? && generation.tools.any?
|
|
205
|
+
details << "#{indent}│ "
|
|
206
|
+
details << "#{indent}│ Tools Available: #{generation.tools.count}"
|
|
207
|
+
generation.tools.first(3).each do |tool|
|
|
208
|
+
tool_name = tool.is_a?(Hash) ? tool["name"] || tool[:name] : tool.to_s
|
|
209
|
+
details << "#{indent}│ - #{tool_name}"
|
|
210
|
+
end
|
|
211
|
+
details << "#{indent}│ ... and #{generation.tools.count - 3} more" if generation.tools.count > 3
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
if generation.input.present?
|
|
215
|
+
details << "#{indent}│ "
|
|
216
|
+
details << "#{indent}│ Input:"
|
|
217
|
+
format_content(generation.input).split("\n").each do |line|
|
|
218
|
+
details << "#{indent}│ #{line}"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
if generation.output.present?
|
|
223
|
+
details << "#{indent}│ "
|
|
224
|
+
details << "#{indent}│ Output:"
|
|
225
|
+
format_content(generation.output).split("\n").each do |line|
|
|
226
|
+
details << "#{indent}│ #{line}"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
if generation.metadata.present? && generation.metadata.any?
|
|
231
|
+
details << "#{indent}│ "
|
|
232
|
+
details << "#{indent}│ Metadata:"
|
|
233
|
+
format_json(generation.metadata).split("\n").each do |line|
|
|
234
|
+
details << "#{indent}│ #{line}"
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
if generation.provider_metadata.present? && generation.provider_metadata.any?
|
|
239
|
+
details << "#{indent}│ "
|
|
240
|
+
details << "#{indent}│ Provider Metadata:"
|
|
241
|
+
format_json(generation.provider_metadata).split("\n").each do |line|
|
|
242
|
+
details << "#{indent}│ #{line}"
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
details.join("\n")
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def format_span_details(span, indent)
|
|
250
|
+
details = []
|
|
251
|
+
|
|
252
|
+
if span.status_message.present?
|
|
253
|
+
details << "#{indent}│ Status: #{span.status_message}"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
if span.input.present?
|
|
257
|
+
details << "#{indent}│ "
|
|
258
|
+
details << "#{indent}│ Input:"
|
|
259
|
+
format_content(span.input).split("\n").each do |line|
|
|
260
|
+
details << "#{indent}│ #{line}"
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
if span.output.present?
|
|
265
|
+
details << "#{indent}│ "
|
|
266
|
+
details << "#{indent}│ Output:"
|
|
267
|
+
format_content(span.output).split("\n").each do |line|
|
|
268
|
+
details << "#{indent}│ #{line}"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
if span.metadata.present? && span.metadata.any?
|
|
273
|
+
details << "#{indent}│ "
|
|
274
|
+
details << "#{indent}│ Metadata:"
|
|
275
|
+
format_json(span.metadata).split("\n").each do |line|
|
|
276
|
+
details << "#{indent}│ #{line}"
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
details.join("\n")
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def format_annotation(annotation, prefix = "")
|
|
284
|
+
parts = []
|
|
285
|
+
timestamp = format_time(annotation.created_at)
|
|
286
|
+
|
|
287
|
+
if annotation.annotator.present?
|
|
288
|
+
parts << "#{prefix}[#{timestamp}] #{annotation.annotator}"
|
|
289
|
+
else
|
|
290
|
+
parts << "#{prefix}[#{timestamp}]"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
parts << "#{prefix}#{annotation.content}"
|
|
294
|
+
|
|
295
|
+
if annotation.tags.present? && annotation.tags.any?
|
|
296
|
+
parts << "#{prefix}Tags: #{annotation.tags.to_json}"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
parts << ""
|
|
300
|
+
parts.join("\n")
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def format_time(time)
|
|
304
|
+
return "N/A" unless time
|
|
305
|
+
time.utc.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def format_duration(duration_ms)
|
|
309
|
+
return "N/A" unless duration_ms
|
|
310
|
+
"#{duration_ms}ms"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def format_cost(cost)
|
|
314
|
+
return "$0.000000" unless cost
|
|
315
|
+
"$#{sprintf('%.6f', cost)}"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def format_content(content)
|
|
319
|
+
return "" if content.blank?
|
|
320
|
+
|
|
321
|
+
# Try to parse as JSON for pretty printing
|
|
322
|
+
if content.is_a?(String)
|
|
323
|
+
begin
|
|
324
|
+
parsed = JSON.parse(content)
|
|
325
|
+
return format_json(parsed)
|
|
326
|
+
rescue JSON::ParserError
|
|
327
|
+
# Not JSON, return as-is
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
truncate_content(content.to_s)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def format_json(obj)
|
|
335
|
+
JSON.pretty_generate(obj)
|
|
336
|
+
rescue StandardError
|
|
337
|
+
obj.to_s
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def truncate_content(content, max_length = MAX_CONTENT_LENGTH)
|
|
341
|
+
return "" if content.blank?
|
|
342
|
+
|
|
343
|
+
content_str = content.to_s
|
|
344
|
+
return content_str if content_str.length <= max_length
|
|
345
|
+
|
|
346
|
+
"#{content_str[0...max_length]}...\n[Content truncated, original length: #{content_str.length} characters]"
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Observ
|
|
4
|
+
class PromptConfigValidator
|
|
5
|
+
attr_reader :config, :errors
|
|
6
|
+
|
|
7
|
+
def initialize(config)
|
|
8
|
+
@config = config
|
|
9
|
+
@errors = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def valid?
|
|
13
|
+
@errors = []
|
|
14
|
+
|
|
15
|
+
return true if config.blank?
|
|
16
|
+
|
|
17
|
+
unless config.is_a?(Hash)
|
|
18
|
+
@errors << "Config must be a Hash"
|
|
19
|
+
return false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
validate_against_schema
|
|
23
|
+
@errors.empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def validate_against_schema
|
|
29
|
+
schema.each do |key, rules|
|
|
30
|
+
value, raw_key = value_with_key(key)
|
|
31
|
+
|
|
32
|
+
# Check required fields
|
|
33
|
+
if rules[:required] && value.nil?
|
|
34
|
+
@errors << "#{key} is required"
|
|
35
|
+
next
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Skip validation if value is nil and not required
|
|
39
|
+
next if value.nil?
|
|
40
|
+
|
|
41
|
+
# Coerce values like numeric strings before validation
|
|
42
|
+
coerced_value = coerce_value(value, rules[:type])
|
|
43
|
+
assign_value(raw_key, coerced_value) if raw_key && coerced_value != value
|
|
44
|
+
value = coerced_value
|
|
45
|
+
|
|
46
|
+
# Validate type
|
|
47
|
+
validate_type(key, value, rules)
|
|
48
|
+
|
|
49
|
+
# Validate range if specified
|
|
50
|
+
validate_range(key, value, rules) if rules[:range]
|
|
51
|
+
|
|
52
|
+
# Validate allowed values if specified
|
|
53
|
+
validate_allowed_values(key, value, rules) if rules[:allowed]
|
|
54
|
+
|
|
55
|
+
# Validate array items if specified
|
|
56
|
+
validate_array_items(key, value, rules) if rules[:item_type]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Check for unknown keys
|
|
60
|
+
validate_unknown_keys if schema_strict?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def validate_type(key, value, rules)
|
|
64
|
+
expected_type = rules[:type]
|
|
65
|
+
|
|
66
|
+
case expected_type
|
|
67
|
+
when :integer
|
|
68
|
+
unless value.is_a?(Integer)
|
|
69
|
+
@errors << "#{key} must be an integer"
|
|
70
|
+
end
|
|
71
|
+
when :float
|
|
72
|
+
unless value.is_a?(Numeric)
|
|
73
|
+
@errors << "#{key} must be a number"
|
|
74
|
+
end
|
|
75
|
+
when :string
|
|
76
|
+
unless value.is_a?(String)
|
|
77
|
+
@errors << "#{key} must be a string"
|
|
78
|
+
end
|
|
79
|
+
when :boolean
|
|
80
|
+
unless [ true, false ].include?(value)
|
|
81
|
+
@errors << "#{key} must be a boolean"
|
|
82
|
+
end
|
|
83
|
+
when :array
|
|
84
|
+
unless value.is_a?(Array)
|
|
85
|
+
@errors << "#{key} must be an array"
|
|
86
|
+
end
|
|
87
|
+
when :hash
|
|
88
|
+
unless value.is_a?(Hash)
|
|
89
|
+
@errors << "#{key} must be a hash"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_range(key, value, rules)
|
|
95
|
+
range = rules[:range]
|
|
96
|
+
|
|
97
|
+
return unless value.is_a?(Numeric)
|
|
98
|
+
|
|
99
|
+
unless range.cover?(value)
|
|
100
|
+
@errors << "#{key} must be between #{range.min} and #{range.max}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def validate_allowed_values(key, value, rules)
|
|
105
|
+
allowed = rules[:allowed]
|
|
106
|
+
|
|
107
|
+
unless allowed.include?(value)
|
|
108
|
+
@errors << "#{key} must be one of: #{allowed.join(', ')}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def validate_array_items(key, value, rules)
|
|
113
|
+
return unless value.is_a?(Array)
|
|
114
|
+
|
|
115
|
+
item_type = rules[:item_type]
|
|
116
|
+
|
|
117
|
+
value.each_with_index do |item, index|
|
|
118
|
+
case item_type
|
|
119
|
+
when :string
|
|
120
|
+
unless item.is_a?(String)
|
|
121
|
+
@errors << "#{key}[#{index}] must be a string"
|
|
122
|
+
end
|
|
123
|
+
when :integer
|
|
124
|
+
unless item.is_a?(Integer)
|
|
125
|
+
@errors << "#{key}[#{index}] must be an integer"
|
|
126
|
+
end
|
|
127
|
+
when :float
|
|
128
|
+
unless item.is_a?(Numeric)
|
|
129
|
+
@errors << "#{key}[#{index}] must be a number"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def validate_unknown_keys
|
|
136
|
+
schema_keys = schema.keys.map { |k| [ k.to_s, k.to_sym ] }.flatten
|
|
137
|
+
config_keys = config.keys
|
|
138
|
+
|
|
139
|
+
unknown_keys = config_keys - schema_keys
|
|
140
|
+
|
|
141
|
+
if unknown_keys.any?
|
|
142
|
+
@errors << "Unknown configuration keys: #{unknown_keys.join(', ')}"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def schema
|
|
147
|
+
@schema ||= Observ.config.prompt_config_schema
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def schema_strict?
|
|
151
|
+
Observ.config.prompt_config_schema_strict
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def value_with_key(key)
|
|
155
|
+
if config.key?(key.to_s)
|
|
156
|
+
[ config[key.to_s], key.to_s ]
|
|
157
|
+
elsif config.key?(key.to_sym)
|
|
158
|
+
[ config[key.to_sym], key.to_sym ]
|
|
159
|
+
else
|
|
160
|
+
[ nil, nil ]
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def assign_value(raw_key, value)
|
|
165
|
+
return unless raw_key
|
|
166
|
+
config[raw_key] = value
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def coerce_value(value, expected_type)
|
|
170
|
+
case expected_type
|
|
171
|
+
when :integer
|
|
172
|
+
return value.to_i if integer_string?(value)
|
|
173
|
+
when :float
|
|
174
|
+
return value.to_f if numeric_string?(value)
|
|
175
|
+
end
|
|
176
|
+
value
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def integer_string?(value)
|
|
180
|
+
value.is_a?(String) && value.match?(/\A-?\d+\z/)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def numeric_string?(value)
|
|
184
|
+
value.is_a?(String) && value.match?(/\A-?\d+(?:\.\d+)?\z/)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%# Link to the "First" page
|
|
2
|
+
- available local variables
|
|
3
|
+
url: url to the first page
|
|
4
|
+
current_page: a page object for the currently displayed page
|
|
5
|
+
total_pages: total number of pages
|
|
6
|
+
per_page: number of items to fetch per page
|
|
7
|
+
remote: data-remote
|
|
8
|
+
-%>
|
|
9
|
+
<span class="first">
|
|
10
|
+
<%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote %>
|
|
11
|
+
</span>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%# Non-link tag that stands for skipped pages...
|
|
2
|
+
- available local variables
|
|
3
|
+
current_page: a page object for the currently displayed page
|
|
4
|
+
total_pages: total number of pages
|
|
5
|
+
per_page: number of items to fetch per page
|
|
6
|
+
remote: data-remote
|
|
7
|
+
-%>
|
|
8
|
+
<span class="page gap"><%= t('views.pagination.truncate').html_safe %></span>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%# Link to the "Last" page
|
|
2
|
+
- available local variables
|
|
3
|
+
url: url to the last page
|
|
4
|
+
current_page: a page object for the currently displayed page
|
|
5
|
+
total_pages: total number of pages
|
|
6
|
+
per_page: number of items to fetch per page
|
|
7
|
+
remote: data-remote
|
|
8
|
+
-%>
|
|
9
|
+
<span class="last">
|
|
10
|
+
<%= link_to_unless current_page.last?, t('views.pagination.last').html_safe, url, remote: remote %>
|
|
11
|
+
</span>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%# Link to the "Next" page
|
|
2
|
+
- available local variables
|
|
3
|
+
url: url to the next page
|
|
4
|
+
current_page: a page object for the currently displayed page
|
|
5
|
+
total_pages: total number of pages
|
|
6
|
+
per_page: number of items to fetch per page
|
|
7
|
+
remote: data-remote
|
|
8
|
+
-%>
|
|
9
|
+
<span class="next">
|
|
10
|
+
<%= link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, rel: 'next', remote: remote %>
|
|
11
|
+
</span>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<%# Link showing page number
|
|
2
|
+
- available local variables
|
|
3
|
+
page: a page object for "this" page
|
|
4
|
+
url: url to this page
|
|
5
|
+
current_page: a page object for the currently displayed page
|
|
6
|
+
total_pages: total number of pages
|
|
7
|
+
per_page: number of items to fetch per page
|
|
8
|
+
remote: data-remote
|
|
9
|
+
-%>
|
|
10
|
+
<span class="page<%= ' current' if page.current? %>">
|
|
11
|
+
<%= link_to_unless page.current?, page, url, {remote: remote, rel: page.rel} %>
|
|
12
|
+
</span>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<%# The container tag
|
|
2
|
+
- available local variables
|
|
3
|
+
current_page: a page object for the currently displayed page
|
|
4
|
+
total_pages: total number of pages
|
|
5
|
+
per_page: number of items to fetch per page
|
|
6
|
+
remote: data-remote
|
|
7
|
+
paginator: the paginator that renders the pagination tags inside
|
|
8
|
+
-%>
|
|
9
|
+
<%= paginator.render do -%>
|
|
10
|
+
<nav class="pagination" role="navigation" aria-label="pager">
|
|
11
|
+
<%= first_page_tag unless current_page.first? %>
|
|
12
|
+
<%= prev_page_tag unless current_page.first? %>
|
|
13
|
+
<% each_page do |page| -%>
|
|
14
|
+
<% if page.display_tag? -%>
|
|
15
|
+
<%= page_tag page %>
|
|
16
|
+
<% elsif !page.was_truncated? -%>
|
|
17
|
+
<%= gap_tag %>
|
|
18
|
+
<% end -%>
|
|
19
|
+
<% end -%>
|
|
20
|
+
<% unless current_page.out_of_range? %>
|
|
21
|
+
<%= next_page_tag unless current_page.last? %>
|
|
22
|
+
<%= last_page_tag unless current_page.last? %>
|
|
23
|
+
<% end %>
|
|
24
|
+
</nav>
|
|
25
|
+
<% end -%>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%# Link to the "Previous" page
|
|
2
|
+
- available local variables
|
|
3
|
+
url: url to the previous page
|
|
4
|
+
current_page: a page object for the currently displayed page
|
|
5
|
+
total_pages: total number of pages
|
|
6
|
+
per_page: number of items to fetch per page
|
|
7
|
+
remote: data-remote
|
|
8
|
+
-%>
|
|
9
|
+
<span class="prev">
|
|
10
|
+
<%= link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, rel: 'prev', remote: remote %>
|
|
11
|
+
</span>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%# Link to the "First" page
|
|
2
|
+
- available local variables
|
|
3
|
+
url: url to the first page
|
|
4
|
+
current_page: a page object for the currently displayed page
|
|
5
|
+
total_pages: total number of pages
|
|
6
|
+
per_page: number of items to fetch per page
|
|
7
|
+
remote: data-remote
|
|
8
|
+
-%>
|
|
9
|
+
<span class="first">
|
|
10
|
+
<%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote %>
|
|
11
|
+
</span>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%# Non-link tag that stands for skipped pages...
|
|
2
|
+
- available local variables
|
|
3
|
+
current_page: a page object for the currently displayed page
|
|
4
|
+
total_pages: total number of pages
|
|
5
|
+
per_page: number of items to fetch per page
|
|
6
|
+
remote: data-remote
|
|
7
|
+
-%>
|
|
8
|
+
<span class="page gap"><%= t('views.pagination.truncate').html_safe %></span>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%# Link to the "Last" page
|
|
2
|
+
- available local variables
|
|
3
|
+
url: url to the last page
|
|
4
|
+
current_page: a page object for the currently displayed page
|
|
5
|
+
total_pages: total number of pages
|
|
6
|
+
per_page: number of items to fetch per page
|
|
7
|
+
remote: data-remote
|
|
8
|
+
-%>
|
|
9
|
+
<span class="last">
|
|
10
|
+
<%= link_to_unless current_page.last?, t('views.pagination.last').html_safe, url, remote: remote %>
|
|
11
|
+
</span>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%# Link to the "Next" page
|
|
2
|
+
- available local variables
|
|
3
|
+
url: url to the next page
|
|
4
|
+
current_page: a page object for the currently displayed page
|
|
5
|
+
total_pages: total number of pages
|
|
6
|
+
per_page: number of items to fetch per page
|
|
7
|
+
remote: data-remote
|
|
8
|
+
-%>
|
|
9
|
+
<span class="next">
|
|
10
|
+
<%= link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, rel: 'next', remote: remote %>
|
|
11
|
+
</span>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<%# Link showing page number
|
|
2
|
+
- available local variables
|
|
3
|
+
page: a page object for "this" page
|
|
4
|
+
url: url to this page
|
|
5
|
+
current_page: a page object for the currently displayed page
|
|
6
|
+
total_pages: total number of pages
|
|
7
|
+
per_page: number of items to fetch per page
|
|
8
|
+
remote: data-remote
|
|
9
|
+
-%>
|
|
10
|
+
<span class="page<%= ' current' if page.current? %>">
|
|
11
|
+
<%= link_to_unless page.current?, page, url, {remote: remote, rel: page.rel} %>
|
|
12
|
+
</span>
|