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,148 @@
|
|
|
1
|
+
module Observ
|
|
2
|
+
class PromptVersionsController < ApplicationController
|
|
3
|
+
before_action :set_prompt_name
|
|
4
|
+
before_action :set_prompt
|
|
5
|
+
|
|
6
|
+
# GET /observ/prompts/:prompt_id/versions/:id
|
|
7
|
+
def show
|
|
8
|
+
redirect_to prompt_path(@prompt_name, version: @prompt.version)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# POST /observ/prompts/:prompt_id/versions/:id/promote
|
|
12
|
+
def promote
|
|
13
|
+
unless @prompt.draft?
|
|
14
|
+
respond_to do |format|
|
|
15
|
+
format.html do
|
|
16
|
+
redirect_to prompt_path(@prompt_name, version: @prompt.version),
|
|
17
|
+
alert: "Only draft prompts can be promoted"
|
|
18
|
+
end
|
|
19
|
+
format.json { render json: { error: "Only draft prompts can be promoted" }, status: :unprocessable_content }
|
|
20
|
+
end
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
Observ::PromptManager.promote(name: @prompt_name, version: @prompt.version)
|
|
26
|
+
|
|
27
|
+
respond_to do |format|
|
|
28
|
+
format.html do
|
|
29
|
+
redirect_to prompt_path(@prompt_name),
|
|
30
|
+
notice: "Version #{@prompt.version} promoted to production"
|
|
31
|
+
end
|
|
32
|
+
format.json { render json: { success: true, message: "Promoted to production" } }
|
|
33
|
+
end
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
respond_to do |format|
|
|
36
|
+
format.html do
|
|
37
|
+
redirect_to prompt_path(@prompt_name, version: @prompt.version),
|
|
38
|
+
alert: e.message
|
|
39
|
+
end
|
|
40
|
+
format.json { render json: { error: e.message }, status: :unprocessable_content }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# POST /observ/prompts/:prompt_id/versions/:id/demote
|
|
46
|
+
def demote
|
|
47
|
+
unless @prompt.production?
|
|
48
|
+
respond_to do |format|
|
|
49
|
+
format.html do
|
|
50
|
+
redirect_to prompt_path(@prompt_name, version: @prompt.version),
|
|
51
|
+
alert: "Only production prompts can be demoted"
|
|
52
|
+
end
|
|
53
|
+
format.json { render json: { error: "Only production prompts can be demoted" }, status: :unprocessable_content }
|
|
54
|
+
end
|
|
55
|
+
return
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
begin
|
|
59
|
+
Observ::PromptManager.demote(name: @prompt_name, version: @prompt.version)
|
|
60
|
+
|
|
61
|
+
respond_to do |format|
|
|
62
|
+
format.html do
|
|
63
|
+
redirect_to prompt_path(@prompt_name),
|
|
64
|
+
notice: "Version #{@prompt.version} demoted to archived"
|
|
65
|
+
end
|
|
66
|
+
format.json { render json: { success: true, message: "Demoted to archived" } }
|
|
67
|
+
end
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
respond_to do |format|
|
|
70
|
+
format.html do
|
|
71
|
+
redirect_to prompt_path(@prompt_name, version: @prompt.version),
|
|
72
|
+
alert: e.message
|
|
73
|
+
end
|
|
74
|
+
format.json { render json: { error: e.message }, status: :unprocessable_content }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# POST /observ/prompts/:prompt_id/versions/:id/restore
|
|
80
|
+
def restore
|
|
81
|
+
unless @prompt.archived?
|
|
82
|
+
respond_to do |format|
|
|
83
|
+
format.html do
|
|
84
|
+
redirect_to prompt_path(@prompt_name, version: @prompt.version),
|
|
85
|
+
alert: "Only archived prompts can be restored"
|
|
86
|
+
end
|
|
87
|
+
format.json { render json: { error: "Only archived prompts can be restored" }, status: :unprocessable_content }
|
|
88
|
+
end
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
Observ::PromptManager.restore(name: @prompt_name, version: @prompt.version)
|
|
94
|
+
|
|
95
|
+
respond_to do |format|
|
|
96
|
+
format.html do
|
|
97
|
+
redirect_to prompt_path(@prompt_name),
|
|
98
|
+
notice: "Version #{@prompt.version} restored to production"
|
|
99
|
+
end
|
|
100
|
+
format.json { render json: { success: true, message: "Restored to production" } }
|
|
101
|
+
end
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
respond_to do |format|
|
|
104
|
+
format.html do
|
|
105
|
+
redirect_to prompt_path(@prompt_name, version: @prompt.version),
|
|
106
|
+
alert: e.message
|
|
107
|
+
end
|
|
108
|
+
format.json { render json: { error: e.message }, status: :unprocessable_content }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# POST /observ/prompts/:prompt_id/versions/:id/clone
|
|
114
|
+
def clone
|
|
115
|
+
# Production and archived prompts are immutable - clone to draft for editing
|
|
116
|
+
begin
|
|
117
|
+
new_prompt = Observ::PromptManager.create(
|
|
118
|
+
name: @prompt_name,
|
|
119
|
+
prompt: @prompt.prompt,
|
|
120
|
+
config: @prompt.config,
|
|
121
|
+
commit_message: "Cloned from version #{@prompt.version}",
|
|
122
|
+
created_by: current_user_identifier,
|
|
123
|
+
promote_to_production: false
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
redirect_to edit_prompt_path(@prompt_name, version: new_prompt.version),
|
|
127
|
+
notice: "Created editable draft (v#{new_prompt.version}) from version #{@prompt.version}"
|
|
128
|
+
rescue ActiveRecord::RecordInvalid, StandardError => e
|
|
129
|
+
redirect_to prompt_path(@prompt_name, version: @prompt.version),
|
|
130
|
+
alert: e.message
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def set_prompt_name
|
|
137
|
+
@prompt_name = params[:prompt_id]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def set_prompt
|
|
141
|
+
@prompt = Observ::Prompt.find(params[:id])
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def current_user_identifier
|
|
145
|
+
"system" # Default fallback
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
module Observ
|
|
2
|
+
class PromptsController < ApplicationController
|
|
3
|
+
before_action :set_prompt_name, only: [ :show, :edit, :update, :destroy, :versions, :compare ]
|
|
4
|
+
before_action :set_prompt, only: [ :edit, :update, :destroy ]
|
|
5
|
+
|
|
6
|
+
# GET /observ/prompts
|
|
7
|
+
def index
|
|
8
|
+
@prompts = Observ::Prompt.select(:name)
|
|
9
|
+
.distinct
|
|
10
|
+
.order(:name)
|
|
11
|
+
|
|
12
|
+
# Apply search filter
|
|
13
|
+
if params[:search].present?
|
|
14
|
+
@prompts = @prompts.where("name LIKE ? COLLATE NOCASE", "%#{params[:search]}%")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Apply state filter
|
|
18
|
+
if params[:state].present?
|
|
19
|
+
@prompts = @prompts.where(state: params[:state])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@prompts = @prompts.page(params[:page]).per(Observ.config.pagination_per_page)
|
|
23
|
+
|
|
24
|
+
# Enrich with metadata for display
|
|
25
|
+
@prompt_data = @prompts.map do |prompt|
|
|
26
|
+
latest = Observ::Prompt.where(name: prompt.name).order(version: :desc).first
|
|
27
|
+
production = Observ::Prompt.where(name: prompt.name, state: :production).first
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
name: prompt.name,
|
|
31
|
+
total_versions: Observ::Prompt.where(name: prompt.name).count,
|
|
32
|
+
production_version: production&.version,
|
|
33
|
+
latest_version: latest.version,
|
|
34
|
+
latest_state: latest.state,
|
|
35
|
+
last_updated: latest.updated_at,
|
|
36
|
+
has_draft: Observ::Prompt.where(name: prompt.name, state: :draft).exists?
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# GET /observ/prompts/new
|
|
42
|
+
def new
|
|
43
|
+
@form = Observ::PromptForm.new(
|
|
44
|
+
name: params[:name],
|
|
45
|
+
from_version: params[:from_version]
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# POST /observ/prompts
|
|
50
|
+
def create
|
|
51
|
+
@form = Observ::PromptForm.new(form_params)
|
|
52
|
+
@form.created_by = current_user_identifier
|
|
53
|
+
|
|
54
|
+
if @form.save
|
|
55
|
+
redirect_to prompt_path(@form.persisted_prompt.name),
|
|
56
|
+
notice: "Prompt created successfully (v#{@form.persisted_prompt.version})"
|
|
57
|
+
else
|
|
58
|
+
render :new, status: :unprocessable_content
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# GET /observ/prompts/:id
|
|
63
|
+
def show
|
|
64
|
+
# Get production version by default, or latest version if no production
|
|
65
|
+
@prompt = Observ::Prompt.where(name: @prompt_name, state: :production).first ||
|
|
66
|
+
Observ::Prompt.where(name: @prompt_name).order(version: :desc).first
|
|
67
|
+
|
|
68
|
+
unless @prompt
|
|
69
|
+
redirect_to prompts_path, alert: "Prompt not found"
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Load specific version if requested
|
|
74
|
+
if params[:version].present?
|
|
75
|
+
@prompt = Observ::Prompt.find_by(name: @prompt_name, version: params[:version])
|
|
76
|
+
unless @prompt
|
|
77
|
+
redirect_to prompt_path(@prompt_name), alert: "Version not found"
|
|
78
|
+
return
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
@all_versions = Observ::Prompt.where(name: @prompt_name).order(version: :desc)
|
|
83
|
+
@production_version = @all_versions.find(&:production?)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# GET /observ/prompts/:id/edit
|
|
87
|
+
def edit
|
|
88
|
+
# Only draft prompts can be edited
|
|
89
|
+
unless @prompt.draft?
|
|
90
|
+
redirect_to prompt_path(@prompt_name),
|
|
91
|
+
alert: "Only draft prompts can be edited. Clone this version to create an editable draft."
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# PATCH /observ/prompts/:id
|
|
96
|
+
def update
|
|
97
|
+
unless @prompt.draft?
|
|
98
|
+
redirect_to prompt_path(@prompt_name),
|
|
99
|
+
alert: "Only draft prompts can be edited"
|
|
100
|
+
return
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Parse config JSON string before updating
|
|
104
|
+
update_params = prompt_params.except(:name, :version, :promote_to_production)
|
|
105
|
+
if update_params[:config].present?
|
|
106
|
+
update_params[:config] = parse_config(update_params[:config])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if @prompt.update(update_params)
|
|
110
|
+
redirect_to prompt_path(@prompt_name, version: @prompt.version),
|
|
111
|
+
notice: "Prompt updated successfully"
|
|
112
|
+
else
|
|
113
|
+
render :edit, status: :unprocessable_content
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# DELETE /observ/prompts/:id
|
|
118
|
+
def destroy
|
|
119
|
+
# Can only delete draft and archived prompts
|
|
120
|
+
if @prompt.production?
|
|
121
|
+
redirect_to prompt_path(@prompt_name),
|
|
122
|
+
alert: "Cannot delete production prompts"
|
|
123
|
+
return
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
@prompt.destroy
|
|
127
|
+
redirect_to prompts_path, notice: "Prompt version #{@prompt.version} deleted"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# GET /observ/prompts/:id/versions
|
|
131
|
+
def versions
|
|
132
|
+
@versions = Observ::Prompt.where(name: @prompt_name).order(version: :desc)
|
|
133
|
+
@production_version = @versions.find(&:production?)
|
|
134
|
+
|
|
135
|
+
respond_to do |format|
|
|
136
|
+
format.html # Render the HTML view
|
|
137
|
+
format.json do
|
|
138
|
+
render json: @versions.as_json(only: [ :version, :state, :commit_message, :created_at ])
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# GET /observ/prompts/:id/compare?from=1&to=2
|
|
144
|
+
def compare
|
|
145
|
+
@from_version = Observ::Prompt.find_by(name: @prompt_name, version: params[:from])
|
|
146
|
+
@to_version = Observ::Prompt.find_by(name: @prompt_name, version: params[:to])
|
|
147
|
+
|
|
148
|
+
unless @from_version && @to_version
|
|
149
|
+
redirect_to versions_prompt_path(@prompt_name),
|
|
150
|
+
alert: "Both versions must be specified"
|
|
151
|
+
return
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
@diff = calculate_diff(@from_version.prompt, @to_version.prompt)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def set_prompt_name
|
|
160
|
+
@prompt_name = params[:id] || params[:name]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def set_prompt
|
|
164
|
+
version = params[:version] || Observ::Prompt.where(name: @prompt_name, state: :draft).maximum(:version)
|
|
165
|
+
@prompt = Observ::Prompt.find_by!(name: @prompt_name, version: version)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def prompt_params
|
|
169
|
+
params.require(:observ_prompt).permit(
|
|
170
|
+
:name, :prompt, :config, :commit_message, :promote_to_production
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def form_params
|
|
175
|
+
params.require(:observ_prompt_form).permit(
|
|
176
|
+
:name, :prompt, :config, :commit_message, :promote_to_production, :from_version
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def parse_config(config_string)
|
|
181
|
+
return {} if config_string.blank?
|
|
182
|
+
JSON.parse(config_string)
|
|
183
|
+
rescue JSON::ParserError
|
|
184
|
+
{}
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def current_user_identifier
|
|
188
|
+
# Implement based on your authentication system
|
|
189
|
+
"system" # Default fallback
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def calculate_diff(text1, text2)
|
|
193
|
+
# Simple line-by-line diff
|
|
194
|
+
# In production, consider using gems like 'diffy' or 'diff-lcs'
|
|
195
|
+
lines1 = text1.split("\n")
|
|
196
|
+
lines2 = text2.split("\n")
|
|
197
|
+
|
|
198
|
+
{
|
|
199
|
+
removed: lines1 - lines2,
|
|
200
|
+
added: lines2 - lines1,
|
|
201
|
+
common: lines1 & lines2
|
|
202
|
+
}
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Observ
|
|
2
|
+
class SessionsController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
@sessions = Observ::Session.order(start_time: :desc)
|
|
5
|
+
apply_filters if params[:filter].present?
|
|
6
|
+
@sessions = @sessions.page(params[:page]).per(Observ.config.pagination_per_page)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
@session = Observ::Session.find(params[:id])
|
|
11
|
+
@traces = @session.traces.order(start_time: :asc)
|
|
12
|
+
@session_metrics = @session.session_metrics
|
|
13
|
+
@chat = @session.chat
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def metrics
|
|
17
|
+
@session = Observ::Session.find(params[:id])
|
|
18
|
+
render json: @session.session_metrics
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def drawer_test
|
|
22
|
+
@session = Observ::Session.find(params[:id])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def annotations_drawer
|
|
26
|
+
@session = Observ::Session.find(params[:id])
|
|
27
|
+
@annotations = @session.annotations.recent
|
|
28
|
+
@annotation = @session.annotations.build
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def apply_filters
|
|
34
|
+
@sessions = @sessions.where(user_id: params[:filter][:user_id]) if params[:filter][:user_id].present?
|
|
35
|
+
|
|
36
|
+
if params[:filter][:start_date].present?
|
|
37
|
+
@sessions = @sessions.where("start_time >= ?", params[:filter][:start_date])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if params[:filter][:end_date].present?
|
|
41
|
+
@sessions = @sessions.where("start_time <= ?", params[:filter][:end_date])
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Observ
|
|
2
|
+
class TracesController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
@traces = Observ::Trace
|
|
5
|
+
.includes(:observ_session)
|
|
6
|
+
.order(start_time: :desc)
|
|
7
|
+
.page(params[:page])
|
|
8
|
+
.per(Observ.config.pagination_per_page)
|
|
9
|
+
|
|
10
|
+
apply_filters if params[:filter].present?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show
|
|
14
|
+
@trace = Observ::Trace.includes(:observations).find(params[:id])
|
|
15
|
+
@observations = @trace.observations.order(start_time: :asc)
|
|
16
|
+
@generations = @trace.generations
|
|
17
|
+
@spans = @trace.spans
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def search
|
|
21
|
+
@traces = Observ::Trace
|
|
22
|
+
.includes(:observ_session)
|
|
23
|
+
.where("trace_id LIKE ? OR name LIKE ?", "%#{params[:q]}%", "%#{params[:q]}%")
|
|
24
|
+
.order(start_time: :desc)
|
|
25
|
+
.limit(50)
|
|
26
|
+
|
|
27
|
+
render :index
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def annotations_drawer
|
|
31
|
+
@trace = Observ::Trace.find(params[:id])
|
|
32
|
+
@annotations = @trace.annotations.recent
|
|
33
|
+
@annotation = @trace.annotations.build
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def text_output_drawer
|
|
37
|
+
@trace = Observ::Trace.includes(:observations, :annotations, observ_session: :annotations).find(params[:id])
|
|
38
|
+
@formatted_text = Observ::TraceTextFormatter.new(@trace).format
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def add_to_dataset_drawer
|
|
42
|
+
@trace = Observ::Trace.find(params[:id])
|
|
43
|
+
@datasets = Observ::Dataset.order(:name)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def add_to_dataset
|
|
47
|
+
@trace = Observ::Trace.find(params[:id])
|
|
48
|
+
@dataset = Observ::Dataset.find(params[:dataset_id])
|
|
49
|
+
|
|
50
|
+
@item = @dataset.items.build(
|
|
51
|
+
input: @trace.input,
|
|
52
|
+
expected_output: params[:expected_output].presence || @trace.output,
|
|
53
|
+
source_trace: @trace,
|
|
54
|
+
status: :active
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if @item.save
|
|
58
|
+
redirect_to dataset_path(@dataset, tab: "items"),
|
|
59
|
+
notice: "Trace added to dataset '#{@dataset.name}' successfully."
|
|
60
|
+
else
|
|
61
|
+
@datasets = Observ::Dataset.order(:name)
|
|
62
|
+
flash.now[:alert] = "Failed to add trace to dataset: #{@item.errors.full_messages.join(', ')}"
|
|
63
|
+
render :add_to_dataset_drawer, status: :unprocessable_entity
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def apply_filters
|
|
70
|
+
if params[:filter][:session_id].present?
|
|
71
|
+
session = Observ::Session.find_by(session_id: params[:filter][:session_id])
|
|
72
|
+
@traces = @traces.where(observ_session_id: session&.id) if session
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
@traces = @traces.where(name: params[:filter][:name]) if params[:filter][:name].present?
|
|
76
|
+
|
|
77
|
+
if params[:filter][:start_date].present?
|
|
78
|
+
@traces = @traces.where("start_time >= ?", params[:filter][:start_date])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if params[:filter][:end_date].present?
|
|
82
|
+
@traces = @traces.where("start_time <= ?", params[:filter][:end_date])
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Observ
|
|
4
|
+
class PromptForm
|
|
5
|
+
include ActiveModel::Model
|
|
6
|
+
include ActiveModel::Attributes
|
|
7
|
+
|
|
8
|
+
attribute :name, :string
|
|
9
|
+
attribute :prompt, :string
|
|
10
|
+
attribute :config, :string # JSON string from form
|
|
11
|
+
attribute :commit_message, :string
|
|
12
|
+
attribute :promote_to_production, :boolean, default: false
|
|
13
|
+
attribute :from_version, :integer
|
|
14
|
+
|
|
15
|
+
# For dependency injection
|
|
16
|
+
attr_accessor :created_by
|
|
17
|
+
attr_reader :persisted_prompt
|
|
18
|
+
|
|
19
|
+
validates :name, presence: true
|
|
20
|
+
validates :prompt, presence: true
|
|
21
|
+
validate :config_must_be_valid_json
|
|
22
|
+
|
|
23
|
+
def initialize(attributes = {})
|
|
24
|
+
super
|
|
25
|
+
load_from_version if from_version.present? && name.present?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def save
|
|
29
|
+
return false unless valid?
|
|
30
|
+
|
|
31
|
+
@persisted_prompt = PromptManager.create(
|
|
32
|
+
name: name,
|
|
33
|
+
prompt: prompt,
|
|
34
|
+
config: parsed_config,
|
|
35
|
+
commit_message: commit_message,
|
|
36
|
+
created_by: created_by,
|
|
37
|
+
promote_to_production: promote_to_production
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
true
|
|
41
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
42
|
+
# Copy model errors to form
|
|
43
|
+
e.record.errors.each do |error|
|
|
44
|
+
errors.add(error.attribute, error.message)
|
|
45
|
+
end
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parsed_config
|
|
50
|
+
return {} if config.blank?
|
|
51
|
+
JSON.parse(config)
|
|
52
|
+
rescue JSON::ParserError
|
|
53
|
+
{}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# For form display - returns formatted JSON string
|
|
57
|
+
def config_json
|
|
58
|
+
return "" if config.blank?
|
|
59
|
+
config.is_a?(String) ? config : JSON.pretty_generate(config)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# ActiveModel compatibility for form_with
|
|
63
|
+
def model_name
|
|
64
|
+
ActiveModel::Name.new(self.class, nil, "observ_prompt_form")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def persisted?
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def to_key
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def to_model
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def config_must_be_valid_json
|
|
82
|
+
return if config.blank?
|
|
83
|
+
JSON.parse(config)
|
|
84
|
+
rescue JSON::ParserError => e
|
|
85
|
+
errors.add(:config, "must be valid JSON: #{e.message}")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def load_from_version
|
|
89
|
+
source = Prompt.find_by(name: name, version: from_version)
|
|
90
|
+
return unless source
|
|
91
|
+
|
|
92
|
+
self.prompt = source.prompt
|
|
93
|
+
self.config = source.config.present? ? JSON.pretty_generate(source.config) : ""
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Observ
|
|
4
|
+
module ChatsHelper
|
|
5
|
+
# Returns formatted agent selection options for the chat form
|
|
6
|
+
#
|
|
7
|
+
# This helper provides agent selection options for use in select dropdowns.
|
|
8
|
+
# The options are memoized per request to avoid redundant agent discovery.
|
|
9
|
+
#
|
|
10
|
+
# Usage in views:
|
|
11
|
+
# <%= form.select :agent_class_name, agent_select_options, {}, class: "observ-select" %>
|
|
12
|
+
#
|
|
13
|
+
# @return [Array<Array<String>>] options array for Rails select helper
|
|
14
|
+
# Format: [["Display Name", "ClassName"], ...]
|
|
15
|
+
def agent_select_options
|
|
16
|
+
@agent_select_options ||= AgentSelectionService.options
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns a map of agent class names to their prompt names
|
|
20
|
+
#
|
|
21
|
+
# This helper identifies which agents use prompt management and maps them
|
|
22
|
+
# to their configured prompt names. Used by the chat form's Stimulus controller
|
|
23
|
+
# to dynamically show/hide the prompt version selector.
|
|
24
|
+
#
|
|
25
|
+
# Usage in views:
|
|
26
|
+
# data: {
|
|
27
|
+
# observ__chat_form_agents_with_prompts_value: agents_with_prompts_map.to_json
|
|
28
|
+
# }
|
|
29
|
+
#
|
|
30
|
+
# @return [Hash<String, String>] Hash mapping agent class names to prompt names
|
|
31
|
+
# Format: { "AgentClassName" => "prompt-name", ... }
|
|
32
|
+
def agents_with_prompts_map
|
|
33
|
+
@agents_with_prompts_map ||= begin
|
|
34
|
+
Observ::AgentProvider.all_agents.each_with_object({}) do |agent_class, hash|
|
|
35
|
+
# Check if agent includes PromptManagement and has prompt management enabled
|
|
36
|
+
if agent_class.included_modules.include?(Observ::PromptManagement) &&
|
|
37
|
+
agent_class.respond_to?(:prompt_management_enabled?) &&
|
|
38
|
+
agent_class.prompt_management_enabled?
|
|
39
|
+
# Extract prompt name from agent's configuration
|
|
40
|
+
prompt_name = agent_class.prompt_config[:prompt_name]
|
|
41
|
+
hash[agent_class.name] = prompt_name if prompt_name.present?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|