activeagent 1.0.1 → 1.0.3
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 +4 -4
- data/README.md +10 -4
- data/lib/active_agent/base.rb +3 -2
- data/lib/active_agent/concerns/provider.rb +6 -2
- data/lib/active_agent/concerns/rescue.rb +39 -0
- data/lib/active_agent/concerns/streaming.rb +2 -1
- data/lib/active_agent/concerns/view.rb +1 -1
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +117 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +54 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +126 -0
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +103 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +56 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +14 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +49 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +65 -0
- data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +77 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +256 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +113 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +208 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +60 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +46 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +125 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +83 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +52 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +169 -0
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +193 -0
- data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +198 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +105 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +135 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +143 -0
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +36 -0
- data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +94 -0
- data/lib/active_agent/dashboard/config/routes.rb +78 -0
- data/lib/active_agent/dashboard/engine.rb +39 -0
- data/lib/active_agent/dashboard.rb +151 -0
- data/lib/active_agent/providers/_base_provider.rb +2 -1
- data/lib/active_agent/providers/anthropic/request.rb +3 -1
- data/lib/active_agent/providers/anthropic/transforms.rb +88 -0
- data/lib/active_agent/providers/anthropic_provider.rb +14 -4
- data/lib/active_agent/providers/azure/_types.rb +5 -0
- data/lib/active_agent/providers/azure/options.rb +111 -0
- data/lib/active_agent/providers/azure_open_ai_provider.rb +2 -0
- data/lib/active_agent/providers/azure_openai_provider.rb +2 -0
- data/lib/active_agent/providers/azure_provider.rb +133 -0
- data/lib/active_agent/providers/azureopenai_provider.rb +2 -0
- data/lib/active_agent/providers/bedrock/_types.rb +8 -0
- data/lib/active_agent/providers/bedrock/bearer_client.rb +109 -0
- data/lib/active_agent/providers/bedrock/options.rb +77 -0
- data/lib/active_agent/providers/bedrock_provider.rb +84 -0
- data/lib/active_agent/providers/common/messages/_types.rb +6 -2
- data/lib/active_agent/providers/concerns/exception_handler.rb +1 -0
- data/lib/active_agent/providers/gemini/_types.rb +19 -0
- data/lib/active_agent/providers/gemini/options.rb +41 -0
- data/lib/active_agent/providers/gemini_provider.rb +94 -0
- data/lib/active_agent/providers/mock/request.rb +1 -4
- data/lib/active_agent/providers/open_ai/chat/transforms.rb +37 -1
- data/lib/active_agent/providers/open_ai/chat_provider.rb +5 -3
- data/lib/active_agent/providers/requesty/_types.rb +16 -0
- data/lib/active_agent/providers/requesty/options.rb +39 -0
- data/lib/active_agent/providers/requesty_provider.rb +63 -0
- data/lib/active_agent/providers/ruby_llm/_types.rb +77 -0
- data/lib/active_agent/providers/ruby_llm/embedding_request.rb +16 -0
- data/lib/active_agent/providers/ruby_llm/messages/_types.rb +109 -0
- data/lib/active_agent/providers/ruby_llm/messages/assistant.rb +27 -0
- data/lib/active_agent/providers/ruby_llm/messages/base.rb +48 -0
- data/lib/active_agent/providers/ruby_llm/messages/system.rb +18 -0
- data/lib/active_agent/providers/ruby_llm/messages/tool.rb +24 -0
- data/lib/active_agent/providers/ruby_llm/messages/user.rb +18 -0
- data/lib/active_agent/providers/ruby_llm/options.rb +28 -0
- data/lib/active_agent/providers/ruby_llm/request.rb +30 -0
- data/lib/active_agent/providers/ruby_llm/tool_proxy.rb +45 -0
- data/lib/active_agent/providers/ruby_llm_provider.rb +407 -0
- data/lib/active_agent/railtie.rb +32 -1
- data/lib/active_agent/telemetry/configuration.rb +213 -0
- data/lib/active_agent/telemetry/instrumentation.rb +155 -0
- data/lib/active_agent/telemetry/reporter.rb +176 -0
- data/lib/active_agent/telemetry/span.rb +267 -0
- data/lib/active_agent/telemetry/tracer.rb +184 -0
- data/lib/active_agent/telemetry.rb +162 -0
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +2 -0
- data/lib/generators/active_agent/dashboard/install/install_generator.rb +96 -0
- data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +89 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +42 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +38 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +22 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +53 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +28 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +43 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +44 -0
- data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +56 -0
- data/lib/generators/active_agent/dashboard/install_generator.rb +64 -0
- data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +30 -0
- data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +30 -0
- metadata +120 -17
|
@@ -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-5-v1:0"
|
|
19
|
+
#
|
|
20
|
+
# @example Agent usage
|
|
21
|
+
# class SummaryAgent < ApplicationAgent
|
|
22
|
+
# generate_with :bedrock, model: "eu.anthropic.claude-sonnet-5-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
|
|
@@ -142,11 +142,15 @@ module ActiveAgent
|
|
|
142
142
|
Common::Messages::Assistant.new(role: "assistant", content: block[:text], name: message.name)
|
|
143
143
|
when "tool_use"
|
|
144
144
|
# Create a message with tool use info as string representation
|
|
145
|
-
|
|
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}"
|
|
146
148
|
Common::Messages::Assistant.new(role: "assistant", content: tool_info, name: message.name)
|
|
147
149
|
when "mcp_tool_use"
|
|
148
150
|
# Create a message with MCP tool use info
|
|
149
|
-
|
|
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}"
|
|
150
154
|
Common::Messages::Assistant.new(role: "assistant", content: tool_info, name: message.name)
|
|
151
155
|
when "mcp_tool_result"
|
|
152
156
|
# Create a message with MCP tool result
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "options"
|
|
4
|
+
require_relative "../open_ai/chat/_types"
|
|
5
|
+
require_relative "../open_ai/embedding/_types"
|
|
6
|
+
|
|
7
|
+
module ActiveAgent
|
|
8
|
+
module Providers
|
|
9
|
+
module Gemini
|
|
10
|
+
# Reuse OpenAI Chat request type (same API format)
|
|
11
|
+
RequestType = OpenAI::Chat::RequestType
|
|
12
|
+
|
|
13
|
+
# Reuse OpenAI Embedding types (same API format)
|
|
14
|
+
module Embedding
|
|
15
|
+
RequestType = OpenAI::Embedding::RequestType
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../open_ai/options"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module Gemini
|
|
8
|
+
# Configuration options for Gemini provider
|
|
9
|
+
#
|
|
10
|
+
# Extends OpenAI::Options with Gemini-specific settings including
|
|
11
|
+
# the default base URL for Gemini's OpenAI-compatible API endpoint.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic configuration
|
|
14
|
+
# options = Options.new(api_key: 'your-api-key')
|
|
15
|
+
#
|
|
16
|
+
# @example With environment variable
|
|
17
|
+
# # Set GEMINI_API_KEY or GOOGLE_API_KEY
|
|
18
|
+
# options = Options.new({})
|
|
19
|
+
#
|
|
20
|
+
# @see https://ai.google.dev/gemini-api/docs/openai
|
|
21
|
+
class Options < ActiveAgent::Providers::OpenAI::Options
|
|
22
|
+
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
|
23
|
+
|
|
24
|
+
attribute :base_url, :string, fallback: GEMINI_BASE_URL
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def resolve_api_key(kwargs)
|
|
29
|
+
kwargs[:api_key] ||
|
|
30
|
+
kwargs[:access_token] ||
|
|
31
|
+
ENV["GEMINI_API_KEY"] ||
|
|
32
|
+
ENV["GOOGLE_API_KEY"]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Not used as part of Gemini
|
|
36
|
+
def resolve_organization_id(kwargs) = nil
|
|
37
|
+
def resolve_project_id(kwargs) = nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require_relative "_base_provider"
|
|
2
|
+
|
|
3
|
+
require_gem!(:openai, __FILE__)
|
|
4
|
+
|
|
5
|
+
require_relative "open_ai_provider"
|
|
6
|
+
require_relative "gemini/_types"
|
|
7
|
+
|
|
8
|
+
module ActiveAgent
|
|
9
|
+
module Providers
|
|
10
|
+
# Provides access to Google's Gemini API via OpenAI-compatible endpoint.
|
|
11
|
+
#
|
|
12
|
+
# Extends OpenAI provider to work with Gemini's OpenAI-compatible API,
|
|
13
|
+
# enabling access to Gemini models through a familiar interface.
|
|
14
|
+
#
|
|
15
|
+
# @see OpenAI::ChatProvider
|
|
16
|
+
# @see https://ai.google.dev/gemini-api/docs/openai
|
|
17
|
+
class GeminiProvider < OpenAI::ChatProvider
|
|
18
|
+
# @return [String]
|
|
19
|
+
def self.service_name
|
|
20
|
+
"Gemini"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Class]
|
|
24
|
+
def self.options_klass
|
|
25
|
+
namespace::Options
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [ActiveModel::Type::Value]
|
|
29
|
+
def self.prompt_request_type
|
|
30
|
+
namespace::RequestType.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [ActiveModel::Type::Value]
|
|
34
|
+
def self.embed_request_type
|
|
35
|
+
namespace::Embedding::RequestType.new
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
protected
|
|
39
|
+
|
|
40
|
+
# Executes chat completion request with Gemini-specific error handling.
|
|
41
|
+
#
|
|
42
|
+
# @see OpenAI::ChatProvider#api_prompt_execute
|
|
43
|
+
# @param parameters [Hash]
|
|
44
|
+
# @return [Object, nil] response object or nil for streaming
|
|
45
|
+
# @raise [OpenAI::Errors::APIConnectionError] when Gemini API unreachable
|
|
46
|
+
def api_prompt_execute(parameters)
|
|
47
|
+
super
|
|
48
|
+
|
|
49
|
+
rescue ::OpenAI::Errors::APIConnectionError => exception
|
|
50
|
+
log_connection_error(exception)
|
|
51
|
+
raise exception
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Executes embedding request with Gemini-specific error handling.
|
|
55
|
+
#
|
|
56
|
+
# @param parameters [Hash]
|
|
57
|
+
# @return [Hash] symbolized API response
|
|
58
|
+
# @raise [OpenAI::Errors::APIConnectionError] when Gemini API unreachable
|
|
59
|
+
def api_embed_execute(parameters)
|
|
60
|
+
client.embeddings.create(**parameters).as_json.deep_symbolize_keys
|
|
61
|
+
rescue ::OpenAI::Errors::APIConnectionError => exception
|
|
62
|
+
log_connection_error(exception)
|
|
63
|
+
raise exception
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Merges streaming delta into the message with role cleanup.
|
|
67
|
+
#
|
|
68
|
+
# Overrides parent to handle Gemini's role copying behavior which duplicates
|
|
69
|
+
# the role field in every streaming chunk, requiring manual cleanup to prevent
|
|
70
|
+
# message corruption.
|
|
71
|
+
#
|
|
72
|
+
# @see OpenAI::ChatProvider#message_merge_delta
|
|
73
|
+
# @param message [Hash]
|
|
74
|
+
# @param delta [Hash]
|
|
75
|
+
# @return [Hash]
|
|
76
|
+
def message_merge_delta(message, delta)
|
|
77
|
+
message[:role] = delta.delete(:role) if delta[:role]
|
|
78
|
+
|
|
79
|
+
hash_merge_delta(message, delta)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Logs connection failures with Gemini API details for debugging.
|
|
83
|
+
#
|
|
84
|
+
# @param error [Exception]
|
|
85
|
+
# @return [void]
|
|
86
|
+
def log_connection_error(error)
|
|
87
|
+
instrument("connection_error.provider.active_agent",
|
|
88
|
+
uri_base: options.base_url,
|
|
89
|
+
exception: error.class,
|
|
90
|
+
message: error.message)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -22,16 +22,13 @@ module ActiveAgent
|
|
|
22
22
|
attribute :stream, :boolean, default: false
|
|
23
23
|
attribute :tools # Array of tool definitions
|
|
24
24
|
attribute :tool_choice # Tool choice configuration
|
|
25
|
+
attribute :response_format
|
|
25
26
|
|
|
26
27
|
# Common Format Compatibility
|
|
27
28
|
def message=(value)
|
|
28
29
|
self.messages ||= []
|
|
29
30
|
self.messages << value
|
|
30
31
|
end
|
|
31
|
-
|
|
32
|
-
def response_format
|
|
33
|
-
{ type: "text" }
|
|
34
|
-
end
|
|
35
32
|
end
|
|
36
33
|
end
|
|
37
34
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "active_support/core_ext/hash/keys"
|
|
4
|
+
require "uri"
|
|
4
5
|
|
|
5
6
|
module ActiveAgent
|
|
6
7
|
module Providers
|
|
@@ -124,10 +125,19 @@ module ActiveAgent
|
|
|
124
125
|
{ type: "text", text: msg_hash[:text] },
|
|
125
126
|
{ type: "image_url", image_url: { url: msg_hash[:image] } }
|
|
126
127
|
]
|
|
128
|
+
elsif msg_hash.key?(:text) && msg_hash.key?(:document)
|
|
129
|
+
# Shorthand with both text and document: { text: "...", document: "url" }
|
|
130
|
+
[
|
|
131
|
+
{ type: "text", text: msg_hash[:text] },
|
|
132
|
+
build_file_content(msg_hash[:document])
|
|
133
|
+
]
|
|
127
134
|
elsif msg_hash.key?(:image)
|
|
128
135
|
# Shorthand with only image: { image: "url" }
|
|
129
136
|
# Text comes from adjacent prompt arguments
|
|
130
137
|
[ { type: "image_url", image_url: { url: msg_hash[:image] } } ]
|
|
138
|
+
elsif msg_hash.key?(:document)
|
|
139
|
+
# Shorthand with only document: { document: "url" }
|
|
140
|
+
[ build_file_content(msg_hash[:document]) ]
|
|
131
141
|
elsif msg_hash.key?(:text)
|
|
132
142
|
# Shorthand: { text: "..." } or { role: "...", text: "..." }
|
|
133
143
|
msg_hash[:text]
|
|
@@ -137,13 +147,39 @@ module ActiveAgent
|
|
|
137
147
|
end
|
|
138
148
|
|
|
139
149
|
# Create appropriate message param based on role and content
|
|
140
|
-
extra_params = msg_hash.except(:role, :content, :text, :image)
|
|
150
|
+
extra_params = msg_hash.except(:role, :content, :text, :image, :document)
|
|
141
151
|
create_message_param(role, content, extra_params)
|
|
142
152
|
else
|
|
143
153
|
raise ArgumentError, "Cannot normalize #{message.class} to message"
|
|
144
154
|
end
|
|
145
155
|
end
|
|
146
156
|
|
|
157
|
+
# Builds a file content block for the Chat API file type
|
|
158
|
+
#
|
|
159
|
+
# Handles both URL and data URI formats:
|
|
160
|
+
# - URL: "http://example.com/document.pdf" → { type: "file", file: { filename: "...", url: "..." } }
|
|
161
|
+
# - Data URI: "data:application/pdf;base64,..." → { type: "file", file: { filename: "...", file_data: "..." } }
|
|
162
|
+
#
|
|
163
|
+
# @param document [String] URL or data URI
|
|
164
|
+
# @return [Hash] file content block
|
|
165
|
+
def build_file_content(document)
|
|
166
|
+
if document.start_with?("data:")
|
|
167
|
+
# Data URI - extract or infer filename from media type
|
|
168
|
+
media_type = document.match(%r{\Adata:([^;,]+)})&.[](1) || "application/octet-stream"
|
|
169
|
+
extension = media_type.split("/").last&.gsub(/[^a-zA-Z0-9]/, "_") || "bin"
|
|
170
|
+
filename = "document.#{extension}"
|
|
171
|
+
{ type: "file", file: { filename: filename, file_data: document } }
|
|
172
|
+
else
|
|
173
|
+
# Regular URL
|
|
174
|
+
filename = File.basename(URI.parse(document).path.to_s)
|
|
175
|
+
filename = "document.pdf" if filename.empty?
|
|
176
|
+
{ type: "file", file: { filename: filename, url: document } }
|
|
177
|
+
end
|
|
178
|
+
rescue URI::Error
|
|
179
|
+
# Fallback for invalid URLs
|
|
180
|
+
{ type: "file", file: { filename: "document.pdf", url: document } }
|
|
181
|
+
end
|
|
182
|
+
|
|
147
183
|
# Creates the appropriate gem message param class for the given role
|
|
148
184
|
#
|
|
149
185
|
# @param role [String] message role (developer, system, user, assistant, tool, function)
|
|
@@ -95,6 +95,8 @@ module ActiveAgent
|
|
|
95
95
|
def process_stream_chunk(api_response_event)
|
|
96
96
|
instrument("stream_chunk.active_agent")
|
|
97
97
|
|
|
98
|
+
broadcast_stream_open
|
|
99
|
+
|
|
98
100
|
# Called Multiple Times: [Chunk<T>, T]<Content, ToolsCall>
|
|
99
101
|
case api_response_event.type
|
|
100
102
|
when :chunk
|
|
@@ -102,7 +104,7 @@ module ActiveAgent
|
|
|
102
104
|
|
|
103
105
|
# If we have a delta, we need to update a message in the stack
|
|
104
106
|
message = find_or_create_message(api_message.index)
|
|
105
|
-
|
|
107
|
+
message_merge_delta(message, api_message.delta.deep_to_h)
|
|
106
108
|
|
|
107
109
|
# Stream back content changes as they come in
|
|
108
110
|
if api_message.delta.content
|
|
@@ -138,7 +140,7 @@ module ActiveAgent
|
|
|
138
140
|
def process_function_calls(api_function_calls)
|
|
139
141
|
api_function_calls.each do |api_function_call|
|
|
140
142
|
content = instrument("tool_call.active_agent", tool_name: api_function_call.dig(:function, :name)) do
|
|
141
|
-
case api_function_call[:type]
|
|
143
|
+
case api_function_call[:type].to_s
|
|
142
144
|
when "function"
|
|
143
145
|
process_tool_call_function(api_function_call[:function])
|
|
144
146
|
else
|
|
@@ -243,7 +245,7 @@ module ActiveAgent
|
|
|
243
245
|
end
|
|
244
246
|
end
|
|
245
247
|
when String
|
|
246
|
-
hash[key] += value
|
|
248
|
+
hash[key] += value.to_s
|
|
247
249
|
else
|
|
248
250
|
hash[key] = value
|
|
249
251
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../open_ai/chat/_types"
|
|
4
|
+
require_relative "options"
|
|
5
|
+
|
|
6
|
+
module ActiveAgent
|
|
7
|
+
module Providers
|
|
8
|
+
module Requesty
|
|
9
|
+
# ActiveModel type for casting and serializing Requesty requests.
|
|
10
|
+
#
|
|
11
|
+
# Requesty is OpenAI-compatible, so requests use the same shape as the
|
|
12
|
+
# OpenAI Chat API. This delegates entirely to OpenAI::Chat::RequestType.
|
|
13
|
+
RequestType = ActiveAgent::Providers::OpenAI::Chat::RequestType
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../open_ai/options"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module Requesty
|
|
8
|
+
# Configuration options for the Requesty provider.
|
|
9
|
+
#
|
|
10
|
+
# Extends OpenAI::Options, overriding the base URL to point at Requesty's
|
|
11
|
+
# OpenAI-compatible gateway and resolving the API key from REQUESTY_API_KEY.
|
|
12
|
+
# Requesty does not use organization or project identifiers.
|
|
13
|
+
#
|
|
14
|
+
# @example Basic configuration
|
|
15
|
+
# options = Options.new(api_key: ENV["REQUESTY_API_KEY"])
|
|
16
|
+
#
|
|
17
|
+
# @see https://docs.requesty.ai
|
|
18
|
+
# @see https://app.requesty.ai/api-keys Requesty API Keys
|
|
19
|
+
class Options < ActiveAgent::Providers::OpenAI::Options
|
|
20
|
+
# @!attribute base_url
|
|
21
|
+
# @return [String] API endpoint (default: "https://router.requesty.ai/v1")
|
|
22
|
+
attribute :base_url, :string, as: "https://router.requesty.ai/v1"
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def resolve_api_key(kwargs)
|
|
27
|
+
kwargs[:api_key] ||
|
|
28
|
+
kwargs[:access_token] ||
|
|
29
|
+
ENV["REQUESTY_API_KEY"] ||
|
|
30
|
+
ENV["REQUESTY_ACCESS_TOKEN"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Not used as part of Requesty
|
|
34
|
+
def resolve_organization_id(kwargs) = nil
|
|
35
|
+
def resolve_project_id(kwargs) = nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require_relative "_base_provider"
|
|
2
|
+
|
|
3
|
+
require_gem!(:openai, __FILE__)
|
|
4
|
+
|
|
5
|
+
require_relative "open_ai_provider"
|
|
6
|
+
require_relative "requesty/_types"
|
|
7
|
+
|
|
8
|
+
module ActiveAgent
|
|
9
|
+
module Providers
|
|
10
|
+
# Provides access to Requesty's OpenAI-compatible LLM gateway.
|
|
11
|
+
#
|
|
12
|
+
# Extends the OpenAI provider to work with Requesty's OpenAI-compatible API,
|
|
13
|
+
# enabling access to multiple AI models through a single interface using the
|
|
14
|
+
# +provider/model+ naming convention (e.g. +openai/gpt-4o-mini+).
|
|
15
|
+
#
|
|
16
|
+
# Requesty is a plain OpenAI-compatible gateway: requests, responses and
|
|
17
|
+
# transforms are identical to the OpenAI Chat API, so this provider reuses
|
|
18
|
+
# OpenAI::Chat::RequestType and OpenAI::Chat::Transforms directly. The only
|
|
19
|
+
# Requesty-specific configuration is the base URL and API key, which live in
|
|
20
|
+
# Requesty::Options.
|
|
21
|
+
#
|
|
22
|
+
# @example Configuration in active_agent.yml
|
|
23
|
+
# requesty:
|
|
24
|
+
# service: "Requesty"
|
|
25
|
+
# api_key: <%= ENV["REQUESTY_API_KEY"] %>
|
|
26
|
+
# model: "openai/gpt-4o-mini"
|
|
27
|
+
#
|
|
28
|
+
# @see OpenAI::ChatProvider
|
|
29
|
+
# @see https://docs.requesty.ai
|
|
30
|
+
class RequestyProvider < OpenAI::ChatProvider
|
|
31
|
+
# @return [String]
|
|
32
|
+
def self.service_name
|
|
33
|
+
"Requesty"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Class]
|
|
37
|
+
def self.options_klass
|
|
38
|
+
Requesty::Options
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [ActiveModel::Type::Value]
|
|
42
|
+
def self.prompt_request_type
|
|
43
|
+
OpenAI::Chat::RequestType.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [ActiveModel::Type::Value]
|
|
47
|
+
def self.embed_request_type
|
|
48
|
+
OpenAI::Embedding::RequestType.new
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
protected
|
|
52
|
+
|
|
53
|
+
# @see BaseProvider#api_response_normalize
|
|
54
|
+
# @param api_response [OpenAI::Models::ChatCompletion]
|
|
55
|
+
# @return [Hash] normalized response hash
|
|
56
|
+
def api_response_normalize(api_response)
|
|
57
|
+
return api_response unless api_response
|
|
58
|
+
|
|
59
|
+
OpenAI::Chat::Transforms.gem_to_hash(api_response)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "options"
|
|
4
|
+
require_relative "request"
|
|
5
|
+
require_relative "embedding_request"
|
|
6
|
+
|
|
7
|
+
module ActiveAgent
|
|
8
|
+
module Providers
|
|
9
|
+
module RubyLLM
|
|
10
|
+
# Type for Request model
|
|
11
|
+
class RequestType < ActiveModel::Type::Value
|
|
12
|
+
def cast(value)
|
|
13
|
+
case value
|
|
14
|
+
when Request
|
|
15
|
+
value
|
|
16
|
+
when Hash
|
|
17
|
+
Request.new(**value.deep_symbolize_keys)
|
|
18
|
+
when nil
|
|
19
|
+
nil
|
|
20
|
+
else
|
|
21
|
+
raise ArgumentError, "Cannot cast #{value.class} to Request"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def serialize(value)
|
|
26
|
+
case value
|
|
27
|
+
when Request
|
|
28
|
+
value.serialize
|
|
29
|
+
when Hash
|
|
30
|
+
value
|
|
31
|
+
when nil
|
|
32
|
+
nil
|
|
33
|
+
else
|
|
34
|
+
raise ArgumentError, "Cannot serialize #{value.class}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def deserialize(value)
|
|
39
|
+
cast(value)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Type for embedding requests
|
|
44
|
+
class EmbeddingRequestType < ActiveModel::Type::Value
|
|
45
|
+
def cast(value)
|
|
46
|
+
case value
|
|
47
|
+
when RubyLLM::EmbeddingRequest
|
|
48
|
+
value
|
|
49
|
+
when Hash
|
|
50
|
+
RubyLLM::EmbeddingRequest.new(**value.deep_symbolize_keys)
|
|
51
|
+
when nil
|
|
52
|
+
nil
|
|
53
|
+
else
|
|
54
|
+
raise ArgumentError, "Cannot cast #{value.class} to EmbeddingRequest"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def serialize(value)
|
|
59
|
+
case value
|
|
60
|
+
when RubyLLM::EmbeddingRequest
|
|
61
|
+
value.serialize
|
|
62
|
+
when Hash
|
|
63
|
+
value
|
|
64
|
+
when nil
|
|
65
|
+
nil
|
|
66
|
+
else
|
|
67
|
+
raise ArgumentError, "Cannot serialize #{value.class}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def deserialize(value)
|
|
72
|
+
cast(value)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_agent/providers/common/model"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module RubyLLM
|
|
8
|
+
# Embedding request model for RubyLLM provider.
|
|
9
|
+
class EmbeddingRequest < Common::BaseModel
|
|
10
|
+
attribute :model, :string
|
|
11
|
+
attribute :input
|
|
12
|
+
attribute :dimensions, :integer
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|