activeagent 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/README.md +10 -4
  4. data/lib/active_agent/base.rb +3 -2
  5. data/lib/active_agent/concerns/provider.rb +6 -2
  6. data/lib/active_agent/concerns/rescue.rb +39 -0
  7. data/lib/active_agent/concerns/streaming.rb +2 -1
  8. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +117 -0
  9. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +54 -0
  10. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +126 -0
  11. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +103 -0
  12. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +56 -0
  13. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +14 -0
  14. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +49 -0
  15. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +65 -0
  16. data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +77 -0
  17. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +256 -0
  18. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +113 -0
  19. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +208 -0
  20. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +60 -0
  21. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +46 -0
  22. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +125 -0
  23. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +83 -0
  24. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +52 -0
  25. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +169 -0
  26. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +193 -0
  27. data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +198 -0
  28. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +105 -0
  29. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +135 -0
  30. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +143 -0
  31. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +36 -0
  32. data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +94 -0
  33. data/lib/active_agent/dashboard/config/routes.rb +78 -0
  34. data/lib/active_agent/dashboard/engine.rb +39 -0
  35. data/lib/active_agent/dashboard.rb +151 -0
  36. data/lib/active_agent/providers/_base_provider.rb +4 -1
  37. data/lib/active_agent/providers/anthropic/options.rb +4 -6
  38. data/lib/active_agent/providers/anthropic/request.rb +28 -3
  39. data/lib/active_agent/providers/anthropic/transforms.rb +131 -2
  40. data/lib/active_agent/providers/anthropic_provider.rb +97 -30
  41. data/lib/active_agent/providers/azure/_types.rb +5 -0
  42. data/lib/active_agent/providers/azure/options.rb +111 -0
  43. data/lib/active_agent/providers/azure_open_ai_provider.rb +2 -0
  44. data/lib/active_agent/providers/azure_openai_provider.rb +2 -0
  45. data/lib/active_agent/providers/azure_provider.rb +133 -0
  46. data/lib/active_agent/providers/azureopenai_provider.rb +2 -0
  47. data/lib/active_agent/providers/bedrock/_types.rb +8 -0
  48. data/lib/active_agent/providers/bedrock/bearer_client.rb +109 -0
  49. data/lib/active_agent/providers/bedrock/options.rb +77 -0
  50. data/lib/active_agent/providers/bedrock_provider.rb +84 -0
  51. data/lib/active_agent/providers/common/messages/_types.rb +42 -31
  52. data/lib/active_agent/providers/common/messages/assistant.rb +20 -4
  53. data/lib/active_agent/providers/concerns/exception_handler.rb +1 -0
  54. data/lib/active_agent/providers/concerns/previewable.rb +39 -5
  55. data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +62 -0
  56. data/lib/active_agent/providers/gemini/_types.rb +19 -0
  57. data/lib/active_agent/providers/gemini/options.rb +41 -0
  58. data/lib/active_agent/providers/gemini_provider.rb +94 -0
  59. data/lib/active_agent/providers/open_ai/chat/transforms.rb +120 -4
  60. data/lib/active_agent/providers/open_ai/chat_provider.rb +40 -0
  61. data/lib/active_agent/providers/open_ai/responses/request.rb +17 -2
  62. data/lib/active_agent/providers/open_ai/responses/transforms.rb +135 -0
  63. data/lib/active_agent/providers/open_ai/responses_provider.rb +38 -0
  64. data/lib/active_agent/providers/open_router/request.rb +20 -0
  65. data/lib/active_agent/providers/open_router/transforms.rb +30 -0
  66. data/lib/active_agent/providers/open_router_provider.rb +14 -0
  67. data/lib/active_agent/providers/ruby_llm/_types.rb +77 -0
  68. data/lib/active_agent/providers/ruby_llm/embedding_request.rb +16 -0
  69. data/lib/active_agent/providers/ruby_llm/messages/_types.rb +109 -0
  70. data/lib/active_agent/providers/ruby_llm/messages/assistant.rb +27 -0
  71. data/lib/active_agent/providers/ruby_llm/messages/base.rb +48 -0
  72. data/lib/active_agent/providers/ruby_llm/messages/system.rb +18 -0
  73. data/lib/active_agent/providers/ruby_llm/messages/tool.rb +24 -0
  74. data/lib/active_agent/providers/ruby_llm/messages/user.rb +18 -0
  75. data/lib/active_agent/providers/ruby_llm/options.rb +28 -0
  76. data/lib/active_agent/providers/ruby_llm/request.rb +30 -0
  77. data/lib/active_agent/providers/ruby_llm/tool_proxy.rb +45 -0
  78. data/lib/active_agent/providers/ruby_llm_provider.rb +407 -0
  79. data/lib/active_agent/railtie.rb +32 -1
  80. data/lib/active_agent/telemetry/configuration.rb +213 -0
  81. data/lib/active_agent/telemetry/instrumentation.rb +155 -0
  82. data/lib/active_agent/telemetry/reporter.rb +176 -0
  83. data/lib/active_agent/telemetry/span.rb +267 -0
  84. data/lib/active_agent/telemetry/tracer.rb +184 -0
  85. data/lib/active_agent/telemetry.rb +162 -0
  86. data/lib/active_agent/version.rb +1 -1
  87. data/lib/active_agent.rb +2 -0
  88. data/lib/generators/active_agent/dashboard/install/install_generator.rb +96 -0
  89. data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +89 -0
  90. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +42 -0
  91. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +38 -0
  92. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +22 -0
  93. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +53 -0
  94. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +28 -0
  95. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +43 -0
  96. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +44 -0
  97. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +56 -0
  98. data/lib/generators/active_agent/dashboard/install_generator.rb +64 -0
  99. data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +30 -0
  100. data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +30 -0
  101. metadata +101 -14
@@ -0,0 +1,133 @@
1
+ require_relative "_base_provider"
2
+
3
+ require_gem!(:openai, __FILE__)
4
+
5
+ require_relative "open_ai_provider"
6
+ require_relative "azure/_types"
7
+
8
+ module ActiveAgent
9
+ module Providers
10
+ # Provider for Azure OpenAI Service via OpenAI-compatible API.
11
+ #
12
+ # Azure OpenAI uses the same API structure as OpenAI but with different
13
+ # authentication (api-key header) and endpoint configuration (resource + deployment).
14
+ #
15
+ # @example Configuration in active_agent.yml
16
+ # azure_openai:
17
+ # service: "AzureOpenAI"
18
+ # api_key: <%= ENV["AZURE_OPENAI_API_KEY"] %>
19
+ # azure_resource: "mycompany"
20
+ # deployment_id: "gpt-4-deployment"
21
+ # api_version: "2024-10-21"
22
+ #
23
+ # @see OpenAI::ChatProvider
24
+ class AzureProvider < OpenAI::ChatProvider
25
+ # @return [String]
26
+ def self.service_name
27
+ "AzureOpenAI"
28
+ end
29
+
30
+ # @return [Class]
31
+ def self.options_klass
32
+ Azure::Options
33
+ end
34
+
35
+ # @return [ActiveModel::Type::Value]
36
+ def self.prompt_request_type
37
+ OpenAI::Chat::RequestType.new
38
+ end
39
+
40
+ # @return [ActiveModel::Type::Value]
41
+ def self.embed_request_type
42
+ OpenAI::Embedding::RequestType.new
43
+ end
44
+
45
+ # Returns a configured Azure OpenAI client.
46
+ #
47
+ # Uses a custom client subclass that handles Azure-specific authentication
48
+ # (api-key header instead of Authorization: Bearer).
49
+ #
50
+ # @return [AzureClient] the configured Azure client
51
+ def client
52
+ @client ||= AzureClient.new(
53
+ api_key: options.api_key,
54
+ base_url: options.base_url,
55
+ api_version: options.api_version,
56
+ max_retries: options.max_retries,
57
+ timeout: options.timeout,
58
+ initial_retry_delay: options.initial_retry_delay,
59
+ max_retry_delay: options.max_retry_delay
60
+ )
61
+ end
62
+
63
+ # Custom OpenAI client for Azure OpenAI Service.
64
+ #
65
+ # Azure uses different authentication headers (api-key instead of Authorization: Bearer)
66
+ # and requires api-version as a query parameter on all requests.
67
+ class AzureClient < ::OpenAI::Client
68
+ # @return [String]
69
+ attr_reader :api_version
70
+
71
+ # Creates a new Azure OpenAI client.
72
+ #
73
+ # @param api_key [String] Azure OpenAI API key
74
+ # @param base_url [String] Azure endpoint URL
75
+ # @param api_version [String] API version (e.g., "2024-10-21")
76
+ # @param max_retries [Integer] Maximum retry attempts
77
+ # @param timeout [Float] Request timeout in seconds
78
+ # @param initial_retry_delay [Float] Initial delay between retries
79
+ # @param max_retry_delay [Float] Maximum delay between retries
80
+ def initialize(
81
+ api_key:,
82
+ base_url:,
83
+ api_version:,
84
+ max_retries: self.class::DEFAULT_MAX_RETRIES,
85
+ timeout: self.class::DEFAULT_TIMEOUT_IN_SECONDS,
86
+ initial_retry_delay: self.class::DEFAULT_INITIAL_RETRY_DELAY,
87
+ max_retry_delay: self.class::DEFAULT_MAX_RETRY_DELAY
88
+ )
89
+ @api_version = api_version
90
+
91
+ super(
92
+ api_key: api_key,
93
+ base_url: base_url,
94
+ max_retries: max_retries,
95
+ timeout: timeout,
96
+ initial_retry_delay: initial_retry_delay,
97
+ max_retry_delay: max_retry_delay
98
+ )
99
+ end
100
+
101
+ private
102
+
103
+ # Azure uses api-key header instead of Authorization: Bearer.
104
+ #
105
+ # @return [Hash{String=>String}]
106
+ def auth_headers
107
+ return {} if @api_key.nil?
108
+
109
+ { "api-key" => @api_key }
110
+ end
111
+
112
+ # Builds request with Azure-specific query parameters.
113
+ #
114
+ # Injects api-version into extra_query for all requests.
115
+ #
116
+ # @param req [Hash] Request parameters
117
+ # @param opts [Hash] Request options
118
+ # @return [Hash] Built request
119
+ def build_request(req, opts)
120
+ # Inject api-version into extra_query
121
+ opts = opts.dup
122
+ opts[:extra_query] = (opts[:extra_query] || {}).merge("api-version" => @api_version)
123
+
124
+ super(req, opts)
125
+ end
126
+ end
127
+ end
128
+
129
+ # Aliases for provider loading with different service name variations
130
+ AzureOpenAIProvider = AzureProvider
131
+ AzureOpenaiProvider = AzureProvider
132
+ end
133
+ end
@@ -0,0 +1,2 @@
1
+ # Azure OpenAI, alternative naming for consistency with other provider aliases
2
+ require_relative "azure_provider"
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "options"
4
+ require_relative "bearer_client"
5
+ require_relative "../anthropic/_types"
6
+
7
+ # Bedrock uses the same request/response types as Anthropic.
8
+ # The BedrockClient handles all protocol translation internally.
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Providers
5
+ module Bedrock
6
+ # Client for AWS Bedrock using bearer token (API key) authentication.
7
+ #
8
+ # Subclasses Anthropic::Client directly to reuse its built-in bearer
9
+ # token support via the +auth_token+ parameter, while adding Bedrock-
10
+ # specific request transformations (URL path rewriting, anthropic_version
11
+ # injection) copied from Anthropic::BedrockClient.
12
+ #
13
+ # This avoids Anthropic::BedrockClient which requires SigV4 credentials
14
+ # and would fail when only a bearer token is available.
15
+ #
16
+ # @see https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys-use.html
17
+ class BearerClient < ::Anthropic::Client
18
+ BEDROCK_VERSION = "bedrock-2023-05-31"
19
+
20
+ # @return [String]
21
+ attr_reader :aws_region
22
+
23
+ # @param aws_region [String] AWS region for the Bedrock endpoint
24
+ # @param bearer_token [String] AWS Bedrock API key (bearer token)
25
+ # @param base_url [String, nil] Override the default Bedrock endpoint
26
+ # @param max_retries [Integer]
27
+ # @param timeout [Float]
28
+ # @param initial_retry_delay [Float]
29
+ # @param max_retry_delay [Float]
30
+ def initialize(
31
+ aws_region:,
32
+ bearer_token:,
33
+ base_url: nil,
34
+ max_retries: self.class::DEFAULT_MAX_RETRIES,
35
+ timeout: self.class::DEFAULT_TIMEOUT_IN_SECONDS,
36
+ initial_retry_delay: self.class::DEFAULT_INITIAL_RETRY_DELAY,
37
+ max_retry_delay: self.class::DEFAULT_MAX_RETRY_DELAY
38
+ )
39
+ @aws_region = aws_region
40
+
41
+ base_url ||= "https://bedrock-runtime.#{aws_region}.amazonaws.com"
42
+
43
+ super(
44
+ auth_token: bearer_token,
45
+ api_key: nil,
46
+ base_url: base_url,
47
+ max_retries: max_retries,
48
+ timeout: timeout,
49
+ initial_retry_delay: initial_retry_delay,
50
+ max_retry_delay: max_retry_delay
51
+ )
52
+
53
+ @messages = ::Anthropic::Resources::Messages.new(client: self)
54
+ @completions = ::Anthropic::Resources::Completions.new(client: self)
55
+ @beta = ::Anthropic::Resources::Beta.new(client: self)
56
+ end
57
+
58
+ private
59
+
60
+ # Intercepts request building to apply Bedrock-specific transformations
61
+ # before the parent class processes the request.
62
+ def build_request(req, opts)
63
+ fit_req_to_bedrock_specs!(req)
64
+ req = super
65
+ body = req.fetch(:body)
66
+ req[:body] = StringIO.new(body.to_a.join) if body.is_a?(Enumerator)
67
+ req
68
+ end
69
+
70
+ # Rewrites Anthropic API paths to Bedrock endpoint paths and injects
71
+ # the Bedrock anthropic_version field.
72
+ #
73
+ # Adapted from Anthropic::Helpers::Bedrock::Client#fit_req_to_bedrock_specs!
74
+ def fit_req_to_bedrock_specs!(request_components)
75
+ if (body = request_components[:body]).is_a?(Hash)
76
+ body[:anthropic_version] ||= BEDROCK_VERSION
77
+ body.transform_keys!("anthropic-beta": :anthropic_beta)
78
+ end
79
+
80
+ case request_components[:path]
81
+ in %r{^v1/messages/batches}
82
+ raise NotImplementedError, "The Batch API is not supported in Bedrock yet"
83
+ in %r{v1/messages/count_tokens}
84
+ raise NotImplementedError, "Token counting is not supported in Bedrock yet"
85
+ in %r{v1/models\?beta=true}
86
+ raise NotImplementedError,
87
+ "Please instead use https://docs.anthropic.com/en/api/claude-on-amazon-bedrock#list-available-models " \
88
+ "to list available models on Bedrock."
89
+ else
90
+ end
91
+
92
+ if %w[
93
+ v1/complete
94
+ v1/messages
95
+ v1/messages?beta=true
96
+ ].include?(request_components[:path]) && request_components[:method] == :post && body.is_a?(Hash)
97
+ model = body.delete(:model)
98
+ model = URI.encode_www_form_component(model.to_s)
99
+ stream = body.delete(:stream) || false
100
+ request_components[:path] =
101
+ stream ? "model/#{model}/invoke-with-response-stream" : "model/#{model}/invoke"
102
+ end
103
+
104
+ request_components
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_agent/providers/common/model"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module Bedrock
8
+ # Configuration for AWS Bedrock provider.
9
+ #
10
+ # AWS credentials are resolved in order:
11
+ # 1. Explicit options (aws_access_key, aws_secret_key)
12
+ # 2. Environment variables (AWS_REGION, AWS_ACCESS_KEY_ID, etc.)
13
+ # 3. AWS SDK default chain (profiles, IAM roles, instance metadata)
14
+ #
15
+ # Unlike the Anthropic provider, no API key is needed — authentication
16
+ # is handled entirely through AWS credentials.
17
+ #
18
+ # @example Minimal config (uses SDK default chain)
19
+ # Bedrock::Options.new(aws_region: "eu-west-2")
20
+ #
21
+ # @example Explicit credentials
22
+ # Bedrock::Options.new(
23
+ # aws_region: "eu-west-2",
24
+ # aws_access_key: "AKIA...",
25
+ # aws_secret_key: "..."
26
+ # )
27
+ #
28
+ # @example With profile
29
+ # Bedrock::Options.new(
30
+ # aws_region: "eu-west-2",
31
+ # aws_profile: "my-profile"
32
+ # )
33
+ class Options < Common::BaseModel
34
+ attribute :aws_region, :string
35
+ attribute :aws_access_key, :string
36
+ attribute :aws_secret_key, :string
37
+ attribute :aws_session_token, :string
38
+ attribute :aws_profile, :string
39
+ attribute :aws_bearer_token, :string
40
+ attribute :base_url, :string
41
+ attribute :anthropic_beta, :string
42
+
43
+ attribute :max_retries, :integer, default: ::Anthropic::Client::DEFAULT_MAX_RETRIES
44
+ attribute :timeout, :float, default: ::Anthropic::Client::DEFAULT_TIMEOUT_IN_SECONDS
45
+ attribute :initial_retry_delay, :float, default: ::Anthropic::Client::DEFAULT_INITIAL_RETRY_DELAY
46
+ attribute :max_retry_delay, :float, default: ::Anthropic::Client::DEFAULT_MAX_RETRY_DELAY
47
+
48
+ def initialize(kwargs = {})
49
+ kwargs = kwargs.deep_symbolize_keys if kwargs.respond_to?(:deep_symbolize_keys)
50
+
51
+ super(**deep_compact(kwargs.except(:default_url_options).merge(
52
+ aws_region: kwargs[:aws_region] || ENV["AWS_REGION"] || ENV["AWS_DEFAULT_REGION"],
53
+ aws_access_key: kwargs[:aws_access_key] || ENV["AWS_ACCESS_KEY_ID"],
54
+ aws_secret_key: kwargs[:aws_secret_key] || ENV["AWS_SECRET_ACCESS_KEY"],
55
+ aws_session_token: kwargs[:aws_session_token] || ENV["AWS_SESSION_TOKEN"],
56
+ aws_profile: kwargs[:aws_profile] || ENV["AWS_PROFILE"],
57
+ aws_bearer_token: kwargs[:aws_bearer_token] || ENV["AWS_BEARER_TOKEN_BEDROCK"]
58
+ )))
59
+ end
60
+
61
+ # Bedrock handles authentication at the client level (SigV4 or bearer token),
62
+ # so no extra headers are needed in request options.
63
+ def extra_headers
64
+ {}
65
+ end
66
+
67
+ # Excludes sensitive AWS credentials from serialized output.
68
+ # The provider's client() method reads credentials directly from options attributes.
69
+ def serialize
70
+ attributes.symbolize_keys.except(
71
+ :aws_access_key, :aws_secret_key, :aws_session_token, :aws_profile, :aws_bearer_token
72
+ )
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "anthropic_provider"
4
+ require_relative "bedrock/_types"
5
+
6
+ module ActiveAgent
7
+ module Providers
8
+ # Provider for Anthropic models hosted on AWS Bedrock.
9
+ #
10
+ # Inherits all functionality from AnthropicProvider (streaming, tool use,
11
+ # multimodal, JSON format emulation) and overrides only the client
12
+ # construction to use Anthropic::BedrockClient for AWS authentication.
13
+ #
14
+ # @example Configuration in active_agent.yml
15
+ # bedrock:
16
+ # service: "Bedrock"
17
+ # aws_region: "eu-west-2"
18
+ # model: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
19
+ #
20
+ # @example Agent usage
21
+ # class SummaryAgent < ApplicationAgent
22
+ # generate_with :bedrock, model: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
23
+ #
24
+ # def summarize
25
+ # prompt(message: params[:message])
26
+ # end
27
+ # end
28
+ #
29
+ # @see AnthropicProvider
30
+ class BedrockProvider < AnthropicProvider
31
+ # @return [String]
32
+ def self.service_name
33
+ "Bedrock"
34
+ end
35
+
36
+ # @return [Class]
37
+ def self.options_klass
38
+ Bedrock::Options
39
+ end
40
+
41
+ # @return [ActiveModel::Type::Value]
42
+ def self.prompt_request_type
43
+ Anthropic::RequestType.new
44
+ end
45
+
46
+ # Returns a configured Bedrock client.
47
+ #
48
+ # When a bearer token is available (via +aws_bearer_token+ option or
49
+ # +AWS_BEARER_TOKEN_BEDROCK+ env var), uses {Bedrock::BearerClient}
50
+ # which sends an +Authorization: Bearer+ header.
51
+ #
52
+ # Otherwise, falls back to {Anthropic::BedrockClient} which handles
53
+ # SigV4 signing, credential resolution, and Bedrock URL path rewriting.
54
+ #
55
+ # @return [Bedrock::BearerClient, Anthropic::Helpers::Bedrock::Client]
56
+ def client
57
+ @client ||= if options.aws_bearer_token.present?
58
+ Bedrock::BearerClient.new(
59
+ aws_region: options.aws_region,
60
+ bearer_token: options.aws_bearer_token,
61
+ base_url: options.base_url.presence,
62
+ max_retries: options.max_retries,
63
+ timeout: options.timeout,
64
+ initial_retry_delay: options.initial_retry_delay,
65
+ max_retry_delay: options.max_retry_delay
66
+ )
67
+ else
68
+ ::Anthropic::BedrockClient.new(
69
+ aws_region: options.aws_region,
70
+ aws_access_key: options.aws_access_key,
71
+ aws_secret_key: options.aws_secret_key,
72
+ aws_session_token: options.aws_session_token,
73
+ aws_profile: options.aws_profile,
74
+ base_url: options.base_url.presence,
75
+ max_retries: options.max_retries,
76
+ timeout: options.timeout,
77
+ initial_retry_delay: options.initial_retry_delay,
78
+ max_retry_delay: options.max_retry_delay
79
+ )
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -37,7 +37,7 @@ module ActiveAgent
37
37
  role = hash[:role]&.to_s
38
38
 
39
39
  case role
40
- when "system"
40
+ when "system", "developer"
41
41
  nil # System messages are dropped in common format, replaced by Instructions
42
42
  when "user", nil
43
43
  # Handle both standard format and format with `text` key
@@ -51,12 +51,6 @@ module ActiveAgent
51
51
  when "assistant"
52
52
  # Filter to only known attributes for Assistant
53
53
  filtered_hash = hash.slice(:role, :content, :name)
54
-
55
- # Compress content array to string if needed (Anthropic format)
56
- if filtered_hash[:content].is_a?(Array)
57
- filtered_hash[:content] = compress_content_array(filtered_hash[:content])
58
- end
59
-
60
54
  Common::Messages::Assistant.new(**filtered_hash)
61
55
  when "tool"
62
56
  # Filter to only known attributes for Tool
@@ -94,29 +88,6 @@ module ActiveAgent
94
88
  raise ArgumentError, "Cannot serialize #{value.class}"
95
89
  end
96
90
  end
97
-
98
- # Compresses Anthropic-style content array into a string.
99
- #
100
- # Anthropic messages can have content as an array of blocks like:
101
- # [{type: "text", text: "..."}, {type: "tool_use", ...}]
102
- # This extracts and joins text blocks into a single string.
103
- #
104
- # @param content_array [Array<Hash>]
105
- # @return [String]
106
- def compress_content_array(content_array)
107
- content_array.map do |block|
108
- case block[:type]&.to_s
109
- when "text"
110
- block[:text]
111
- when "tool_use"
112
- # Tool use blocks don't have readable text content
113
- nil
114
- else
115
- # Unknown block type, try to extract text if present
116
- block[:text]
117
- end
118
- end.compact.join("\n")
119
- end
120
91
  end
121
92
 
122
93
  # Type for Messages array
@@ -124,7 +95,9 @@ module ActiveAgent
124
95
  def cast(value)
125
96
  case value
126
97
  when Array
127
- value.map { |v| message_type.cast(v) }.compact
98
+ messages = value.map { |v| message_type.cast(v) }.compact
99
+ # Split messages with array content into separate messages
100
+ messages.flat_map { |msg| split_content_blocks(msg) }
128
101
  when nil
129
102
  []
130
103
  else
@@ -152,6 +125,44 @@ module ActiveAgent
152
125
  def message_type
153
126
  @message_type ||= MessageType.new
154
127
  end
128
+
129
+ # Splits an assistant message with array content into separate messages
130
+ # for each content block.
131
+ #
132
+ # @param message [Common::Messages::Base]
133
+ # @return [Array<Common::Messages::Base>]
134
+ def split_content_blocks(message)
135
+ # Only split assistant messages with array content
136
+ return [ message ] unless message.is_a?(Common::Messages::Assistant) && message.content.is_a?(Array)
137
+
138
+ message.content.map do |block|
139
+ case block[:type]&.to_s
140
+ when "text"
141
+ # Create a message for text blocks
142
+ Common::Messages::Assistant.new(role: "assistant", content: block[:text], name: message.name)
143
+ when "tool_use"
144
+ # Create a message with tool use info as string representation
145
+ input = block[:input]
146
+ input_str = input.nil? || input.empty? ? "{}" : JSON.pretty_generate(input)
147
+ tool_info = "[Tool Use: #{block[:name]}]\nID: #{block[:id]}\nInput: #{input_str}"
148
+ Common::Messages::Assistant.new(role: "assistant", content: tool_info, name: message.name)
149
+ when "mcp_tool_use"
150
+ # Create a message with MCP tool use info
151
+ input = block[:input]
152
+ input_str = input.nil? || input.empty? ? "{}" : JSON.pretty_generate(input)
153
+ tool_info = "[MCP Tool Use: #{block[:name]}]\nID: #{block[:id]}\nServer: #{block[:server_name]}\nInput: #{input_str}"
154
+ Common::Messages::Assistant.new(role: "assistant", content: tool_info, name: message.name)
155
+ when "mcp_tool_result"
156
+ # Create a message with MCP tool result
157
+ result_info = "[MCP Tool Result]\n#{block[:content]}"
158
+ Common::Messages::Assistant.new(role: "assistant", content: result_info, name: message.name)
159
+ else
160
+ # For unknown block types, try to extract text
161
+ content = block[:text] || block.to_s
162
+ Common::Messages::Assistant.new(role: "assistant", content:, name: message.name)
163
+ end
164
+ end.compact
165
+ end
155
166
  end
156
167
  end
157
168
  end
@@ -9,7 +9,7 @@ module ActiveAgent
9
9
  # Represents messages sent by the AI assistant in a conversation.
10
10
  class Assistant < Base
11
11
  attribute :role, :string, as: "assistant"
12
- attribute :content, :string
12
+ attribute :content # Accept both string and array (provider-native formats)
13
13
  attribute :name, :string
14
14
 
15
15
  validates :content, presence: true
@@ -24,9 +24,16 @@ module ActiveAgent
24
24
  # @param normalize_names [Symbol, nil] key normalization method (e.g., :underscore)
25
25
  # @return [Hash, Array, nil] parsed JSON structure or nil if parsing fails
26
26
  def parsed_json(symbolize_names: true, normalize_names: :underscore)
27
- start_char = [ content.index("{"), content.index("[") ].compact.min
28
- end_char = [ content.rindex("}"), content.rindex("]") ].compact.max
29
- content_stripped = content[start_char..end_char] if start_char && end_char
27
+ # Handle array content (from content blocks) by searching through each block
28
+ content_str = if content.is_a?(Array)
29
+ content.map { |block| block.is_a?(Hash) ? block[:text] : block.to_s }.join("\n")
30
+ else
31
+ content.to_s
32
+ end
33
+
34
+ start_char = [ content_str.index("{"), content_str.index("[") ].compact.min
35
+ end_char = [ content_str.rindex("}"), content_str.rindex("]") ].compact.max
36
+ content_stripped = content_str[start_char..end_char] if start_char && end_char
30
37
  return unless content_stripped
31
38
 
32
39
  content_parsed = JSON.parse(content_stripped)
@@ -48,6 +55,15 @@ module ActiveAgent
48
55
  nil
49
56
  end
50
57
 
58
+ # Returns content as a string, handling both string and array formats
59
+ def text
60
+ if content.is_a?(Array)
61
+ content.map { |block| block.is_a?(Hash) ? block[:text] : block.to_s }.join("\n")
62
+ else
63
+ content.to_s
64
+ end
65
+ end
66
+
51
67
  alias_method :json_object, :parsed_json
52
68
  alias_method :parse_json, :parsed_json
53
69
  end
@@ -55,6 +55,7 @@ module ActiveAgent
55
55
  yield
56
56
  rescue => exception
57
57
  rescue_with_handler(exception) || raise
58
+ nil # Discard handler return value to prevent polluting raw_response
58
59
  end
59
60
 
60
61
  # Bubbles up exceptions to the Agent's rescue_from if a handler is defined.
@@ -86,7 +86,13 @@ module ActiveAgent
86
86
  "### Message #{index} (#{role.capitalize})\n#{content}"
87
87
  end
88
88
 
89
- # Renders available tools with descriptions and parameter schemas.
89
+ # Renders tools section for preview.
90
+ #
91
+ # Handles multiple tool formats:
92
+ # - Common format: {name: "...", description: "...", parameters: {...}}
93
+ # - Anthropic format: {name: "...", description: "...", input_schema: {...}}
94
+ # - Chat API format: {type: "function", function: {name: "...", description: "...", parameters: {...}}}
95
+ # - Responses API format: {type: "function", name: "...", description: "...", parameters: {...}}
90
96
  #
91
97
  # @param tools [Array<Hash>]
92
98
  # @return [String]
@@ -96,17 +102,45 @@ module ActiveAgent
96
102
  content = +"## Tools\n\n"
97
103
 
98
104
  tools.each_with_index do |tool, index|
99
- content << "### #{tool[:name] || "Tool #{index + 1}"}\n"
100
- content << "**Description:** #{tool[:description] || 'No description'}\n\n"
105
+ # Extract name and description from different formats
106
+ tool_name, tool_description, tool_params = extract_tool_details(tool)
107
+
108
+ content << "### #{tool_name || "Tool #{index + 1}"}\n"
109
+ content << "**Description:** #{tool_description || 'No description'}\n\n"
101
110
 
102
- if tool[:parameters]
103
- content << "**Parameters:**\n```json\n#{JSON.pretty_generate(tool[:parameters])}\n```\n\n"
111
+ if tool_params
112
+ content << "**Parameters:**\n```json\n#{JSON.pretty_generate(tool_params)}\n```\n\n"
104
113
  end
105
114
  end
106
115
 
107
116
  content.chomp
108
117
  end
109
118
 
119
+ # Extracts tool details from different formats.
120
+ #
121
+ # @param tool [Hash]
122
+ # @return [Array<String, String, Hash>] [name, description, parameters]
123
+ def extract_tool_details(tool)
124
+ tool_hash = tool.is_a?(Hash) ? tool : {}
125
+
126
+ # Chat API nested format: {type: "function", function: {...}}
127
+ if tool_hash[:type] == "function" && tool_hash[:function]
128
+ func = tool_hash[:function]
129
+ return [
130
+ func[:name],
131
+ func[:description],
132
+ func[:parameters] || func[:input_schema]
133
+ ]
134
+ end
135
+
136
+ # Flat formats (common, Anthropic, Responses)
137
+ [
138
+ tool_hash[:name],
139
+ tool_hash[:description],
140
+ tool_hash[:parameters] || tool_hash[:input_schema]
141
+ ]
142
+ end
143
+
110
144
  # Extracts text content from various message formats.
111
145
  #
112
146
  # Handles string messages, hash messages with :content key, and