actionagent 1.9.0 → 1.9.1

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.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionAgent
4
+ module Api
5
+ # Server-to-server submission; end-user apps authenticate their own users
6
+ # and proxy feedback rather than exposing the ingest key in a browser.
7
+ class FeedbackIngestController < ActionController::API
8
+ include IngestAuthentication
9
+
10
+ def create
11
+ envelope = params.require(:feedback)
12
+ raise ActionController::ParameterMissing, :feedback unless envelope.is_a?(ActionController::Parameters)
13
+
14
+ input = envelope.permit(:trace_id, :feedback_id, :rating, :comment, :span_id, :user_reference)
15
+ trace = ActionAgent.trace_model.for_account(@account).find_by!(trace_id: input.delete(:trace_id))
16
+ record = nil
17
+ # Serialize concurrent changes and use the unique index for first-write
18
+ # races. Reusing an ID deliberately replaces the prior rating/comment.
19
+ trace.with_lock do
20
+ record = InteractionFeedback.find_or_initialize_by(
21
+ telemetry_trace_id: trace.id, feedback_id: input[:feedback_id])
22
+ record.update!(input)
23
+ end
24
+ render json: { feedback: record.as_json }, status: :ok
25
+ rescue ActiveRecord::RecordNotFound
26
+ render json: { error: "Trace not found; retry after trace ingestion completes" }, status: :not_found
27
+ rescue ActiveRecord::RecordInvalid => e
28
+ render json: { error: e.record.errors.full_messages }, status: :unprocessable_entity
29
+ rescue ActionController::ParameterMissing => e
30
+ render json: { error: e.message }, status: :bad_request
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionAgent
4
+ module Api
5
+ class FeedbacksController < BaseController
6
+ before_action :require_owner!
7
+
8
+ def index
9
+ traces = owned_traces
10
+ traces = owner_agents.find(params[:agent_id]).telemetry_traces(traces) if params[:agent_id].present?
11
+ scope = InteractionFeedback.where(telemetry_trace_id: traces.select(:id))
12
+ if params[:rating].present?
13
+ return render(json: { error: "Invalid rating" }, status: :bad_request) unless %w[positive negative].include?(params[:rating])
14
+ scope = scope.where(rating: params[:rating])
15
+ end
16
+ scope = scope.where("created_at >= ?", date_start(params[:from])) if params[:from].present?
17
+ scope = scope.where("created_at < ?", date_start(params[:to]) + 1.day) if params[:to].present?
18
+ counts = scope.group(:rating).count
19
+ total = counts.values.sum
20
+ page = clamped_param(:page, default: 1, min: 1, max: 100_000)
21
+ rows = scope.order(created_at: :desc, id: :desc).offset((page - 1) * 50).limit(50)
22
+ trace_ids = ActionAgent.trace_model.where(id: rows.map(&:telemetry_trace_id)).pluck(:id, :trace_id).to_h
23
+ render json: {
24
+ feedback: rows.map { |row| row.as_json.merge("trace_id" => trace_ids[row.telemetry_trace_id]) },
25
+ page: page, total: total, positive: counts.fetch("positive", 0), negative: counts.fetch("negative", 0),
26
+ positive_percentage: total.zero? ? nil : (100.0 * counts.fetch("positive", 0) / total).round(1)
27
+ }
28
+ rescue Date::Error
29
+ render json: { error: "Dates must use YYYY-MM-DD" }, status: :bad_request
30
+ end
31
+
32
+ private
33
+
34
+ def date_start(value)
35
+ date = Date.iso8601(value.to_s)
36
+ Time.zone.local(date.year, date.month, date.day)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionAgent
4
+ # Feedback is attached to a trace primary key, so tenant ownership follows
5
+ # the trace even when two tenants report the same external trace_id.
6
+ class InteractionFeedback < ApplicationRecord
7
+ belongs_to :telemetry_trace, class_name: "ActionAgent::TelemetryTrace"
8
+ validates :feedback_id, presence: true, length: { maximum: 128 }, uniqueness: { scope: :telemetry_trace_id }
9
+ validates :rating, inclusion: { in: %w[positive negative] }
10
+ validates :comment, length: { maximum: 5000 }
11
+ validates :span_id, length: { maximum: 128 }
12
+ validates :user_reference, length: { maximum: 255 }
13
+ validate :span_belongs_to_trace
14
+
15
+ private
16
+
17
+ def span_belongs_to_trace
18
+ return if span_id.blank? || telemetry_trace.nil?
19
+ return if Array(telemetry_trace.spans).any? { |span| span["span_id"] == span_id }
20
+
21
+ errors.add(:span_id, "must belong to the trace")
22
+ end
23
+ end
24
+ end
data/config/routes.rb CHANGED
@@ -32,6 +32,9 @@ ActionAgent::Engine.routes.draw do
32
32
  # Authenticated like trace ingest, with a bearer token.
33
33
  resources :evaluation_reports, only: [ :create ]
34
34
 
35
+ post "feedback", to: "feedback_ingest#create"
36
+ get "feedback", to: "feedbacks#index"
37
+
35
38
  # A JSON API has no :new or :edit forms to serve.
36
39
  resources :agents, except: [ :new, :edit ] do
37
40
  member do
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionAgent
4
- VERSION = "1.9.0"
4
+ VERSION = "1.9.1"
5
5
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateActiveAgentInteractionFeedbacks < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table "#{ActionAgent.table_name_prefix}interaction_feedbacks" do |t|
6
+ t.references :telemetry_trace, null: false, foreign_key: { to_table: :active_agent_telemetry_traces, on_delete: :cascade }
7
+ t.string :feedback_id, null: false, limit: 128
8
+ t.string :rating, null: false, limit: 16
9
+ t.string :span_id, limit: 128
10
+ t.string :user_reference, limit: 255
11
+ t.text :comment
12
+ t.timestamps
13
+ end
14
+ add_index "#{ActionAgent.table_name_prefix}interaction_feedbacks",
15
+ [ :telemetry_trace_id, :feedback_id ], unique: true, name: "index_feedback_on_trace_and_feedback_id"
16
+ end
17
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionagent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-15 00:00:00.000000000 Z
11
+ date: 2026-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activeagent
@@ -139,6 +139,8 @@ files:
139
139
  - app/controllers/action_agent/api/evaluation_reports_controller.rb
140
140
  - app/controllers/action_agent/api/evaluations_controller.rb
141
141
  - app/controllers/action_agent/api/explorations_controller.rb
142
+ - app/controllers/action_agent/api/feedback_ingest_controller.rb
143
+ - app/controllers/action_agent/api/feedbacks_controller.rb
142
144
  - app/controllers/action_agent/api/github_app_manifests_controller.rb
143
145
  - app/controllers/action_agent/api/github_connections_controller.rb
144
146
  - app/controllers/action_agent/api/github_installations_controller.rb
@@ -221,6 +223,7 @@ files:
221
223
  - app/models/action_agent/github_connection.rb
222
224
  - app/models/action_agent/github_installation.rb
223
225
  - app/models/action_agent/input_request.rb
226
+ - app/models/action_agent/interaction_feedback.rb
224
227
  - app/models/action_agent/model_pricing.rb
225
228
  - app/models/action_agent/project.rb
226
229
  - app/models/action_agent/project_secret.rb
@@ -354,6 +357,7 @@ files:
354
357
  - lib/generators/action_agent/templates/migrations/024_add_kind_to_active_agent_project_secrets.rb.erb
355
358
  - lib/generators/action_agent/templates/migrations/025_create_active_agent_scenario_catalogs.rb.erb
356
359
  - lib/generators/action_agent/templates/migrations/026_add_evaluation_fixes_to_code_sessions.rb.erb
360
+ - lib/generators/action_agent/templates/migrations/027_create_active_agent_interaction_feedbacks.rb.erb
357
361
  - lib/tasks/action_agent.rake
358
362
  - lib/tasks/browser.rake
359
363
  - lib/tasks/claude_code.rake