activeagent 1.8.0 → 1.9.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 +4 -4
- data/CHANGELOG.md +1193 -0
- data/README.md +9 -1
- data/lib/active_agent/base.rb +22 -1
- data/lib/active_agent/concerns/delegation.rb +4 -0
- data/lib/active_agent/concerns/input_requests.rb +178 -0
- data/lib/active_agent/concerns/parameterized.rb +7 -4
- data/lib/active_agent/concerns/provider.rb +1 -0
- data/lib/active_agent/concerns/tooling.rb +7 -2
- data/lib/active_agent/delegation/runner.rb +31 -0
- data/lib/active_agent/evals/catalog.rb +305 -0
- data/lib/active_agent/evals/diagnosis.rb +3 -3
- data/lib/active_agent/evals/format.rb +115 -0
- data/lib/active_agent/evals/judge.rb +2 -0
- data/lib/active_agent/evals/report.rb +232 -31
- data/lib/active_agent/evals/report_html.rb +316 -149
- data/lib/active_agent/evals/result.rb +40 -0
- data/lib/active_agent/evals/ruby_llm.rb +411 -0
- data/lib/active_agent/evals/runner.rb +6 -2
- data/lib/active_agent/evals/scenario.rb +10 -3
- data/lib/active_agent/evals/scenario_parser.rb +38 -10
- data/lib/active_agent/evals.rb +2 -0
- data/lib/active_agent/generation.rb +62 -2
- data/lib/active_agent/generation_job.rb +14 -3
- data/lib/active_agent/input_request/resume.rb +199 -0
- data/lib/active_agent/input_request.rb +279 -0
- data/lib/active_agent/providers/_base_provider.rb +153 -31
- data/lib/active_agent/providers/anthropic/transforms.rb +57 -28
- data/lib/active_agent/providers/anthropic_provider.rb +102 -20
- data/lib/active_agent/providers/common/responses/prompt.rb +21 -0
- data/lib/active_agent/providers/concerns/input_requests.rb +332 -0
- data/lib/active_agent/providers/concerns/mcp_serving.rb +267 -0
- data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +18 -3
- data/lib/active_agent/providers/deep_seek_provider.rb +6 -0
- data/lib/active_agent/providers/deepseek/_types.rb +4 -0
- data/lib/active_agent/providers/deepseek/chat/_types.rb +54 -0
- data/lib/active_agent/providers/deepseek/chat/request.rb +121 -0
- data/lib/active_agent/providers/deepseek/options.rb +53 -0
- data/lib/active_agent/providers/deepseek_provider.rb +46 -0
- data/lib/active_agent/providers/log_subscriber.rb +13 -0
- data/lib/active_agent/providers/mcp_bridge.rb +691 -0
- data/lib/active_agent/providers/mcp_tool_cache.rb +230 -0
- data/lib/active_agent/providers/open_ai/_base.rb +1 -1
- data/lib/active_agent/providers/open_ai/chat_provider.rb +22 -3
- data/lib/active_agent/providers/open_ai/options.rb +1 -1
- data/lib/active_agent/providers/open_ai/responses_provider.rb +68 -15
- data/lib/active_agent/providers/open_router/options.rb +6 -2
- data/lib/active_agent/providers/ruby_llm/messages/_types.rb +3 -1
- data/lib/active_agent/providers/ruby_llm/options.rb +5 -0
- data/lib/active_agent/providers/ruby_llm_provider.rb +127 -29
- data/lib/active_agent/schema_tools.rb +12 -0
- data/lib/active_agent/telemetry/instrumentation.rb +10 -1
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +2 -0
- data/lib/generators/active_agent/schema_tools/USAGE +11 -0
- data/lib/generators/active_agent/schema_tools/schema_tools_generator.rb +39 -4
- data/lib/generators/active_agent/schema_tools/templates/schema_tools.rb.tt +13 -0
- metadata +32 -2
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "request"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module DeepSeek
|
|
8
|
+
module Chat
|
|
9
|
+
# ActiveModel type for casting and serializing DeepSeek chat requests.
|
|
10
|
+
#
|
|
11
|
+
# @see ActiveAgent::Providers::Ollama::Chat::RequestType
|
|
12
|
+
class RequestType < ActiveModel::Type::Value
|
|
13
|
+
# @param value [Request, Hash, nil]
|
|
14
|
+
# @return [Request, nil]
|
|
15
|
+
# @raise [ArgumentError] if value cannot be cast
|
|
16
|
+
def cast(value)
|
|
17
|
+
case value
|
|
18
|
+
when Request
|
|
19
|
+
value
|
|
20
|
+
when Hash
|
|
21
|
+
Request.new(**value.deep_symbolize_keys)
|
|
22
|
+
when nil
|
|
23
|
+
nil
|
|
24
|
+
else
|
|
25
|
+
raise ArgumentError, "Cannot cast #{value.class} to Request"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param value [Request, Hash, nil]
|
|
30
|
+
# @return [Hash, nil]
|
|
31
|
+
# @raise [ArgumentError] if value cannot be serialized
|
|
32
|
+
def serialize(value)
|
|
33
|
+
case value
|
|
34
|
+
when Request
|
|
35
|
+
value.serialize
|
|
36
|
+
when Hash
|
|
37
|
+
value
|
|
38
|
+
when nil
|
|
39
|
+
nil
|
|
40
|
+
else
|
|
41
|
+
raise ArgumentError, "Cannot serialize #{value.class}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param value [Object]
|
|
46
|
+
# @return [Request, nil]
|
|
47
|
+
def deserialize(value)
|
|
48
|
+
cast(value)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../open_ai/chat/request"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module DeepSeek
|
|
8
|
+
module Chat
|
|
9
|
+
# Chat completion request for DeepSeek.
|
|
10
|
+
#
|
|
11
|
+
# Extends OpenAI::Chat::Request with DeepSeek's defaults for the two
|
|
12
|
+
# things OpenAI cannot supply: the model name and the message roles.
|
|
13
|
+
#
|
|
14
|
+
# Everything else is left to DeepSeek. Its API is the authority on how
|
|
15
|
+
# it wants to be called, and a library that second-guesses that gets
|
|
16
|
+
# stale as the provider tunes itself — thinking mode, for one, is on by
|
|
17
|
+
# default and is the provider's call to make.
|
|
18
|
+
#
|
|
19
|
+
# Thinking is worth knowing about either way, because it changes what
|
|
20
|
+
# the other parameters do: DeepSeek bills the reasoning whether or not
|
|
21
|
+
# the answer needed it, and it ignores temperature, presence_penalty and
|
|
22
|
+
# frequency_penalty while thinking. A one-line JSON extraction measured
|
|
23
|
+
# 83 output tokens at DeepSeek's default against 7 with thinking
|
|
24
|
+
# disabled, so a prompt that wants those sampling parameters to bite, or
|
|
25
|
+
# wants to skip paying for reasoning it discards, can opt out itself:
|
|
26
|
+
#
|
|
27
|
+
# prompt "…", thinking: { type: "disabled" }
|
|
28
|
+
#
|
|
29
|
+
# @see OpenAI::Chat::Request
|
|
30
|
+
# @see https://api-docs.deepseek.com/guides/thinking_mode
|
|
31
|
+
class Request < OpenAI::Chat::Request
|
|
32
|
+
# Model used when a prompt does not name one. DeepSeek has no default
|
|
33
|
+
# of its own — the API requires `model` — so this is a convenience,
|
|
34
|
+
# not an override.
|
|
35
|
+
DEFAULT_MODEL = "deepseek-flash"
|
|
36
|
+
|
|
37
|
+
# @param params [Hash] request parameters
|
|
38
|
+
# @option params [String] :model (deepseek-flash)
|
|
39
|
+
# @option params [Hash] :thinking DeepSeek's own thinking toggle,
|
|
40
|
+
# passed through untouched; `{ type: "disabled" }` opts out
|
|
41
|
+
def initialize(**params)
|
|
42
|
+
params[:model] ||= DEFAULT_MODEL
|
|
43
|
+
|
|
44
|
+
demote_developer_roles!(params)
|
|
45
|
+
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Rewrites `developer` messages to `system` before the OpenAI
|
|
52
|
+
# transforms run.
|
|
53
|
+
#
|
|
54
|
+
# DeepSeek accepts system, user, assistant, tool and latest_reminder,
|
|
55
|
+
# and nothing else. It rejects the rest rather than ignoring them:
|
|
56
|
+
#
|
|
57
|
+
# messages[0].role: unknown variant `developer`, expected one of
|
|
58
|
+
# `system`, `user`, `assistant`, `tool`, `latest_reminder`
|
|
59
|
+
#
|
|
60
|
+
# That is a 422 on the whole request, and `developer` is the role it
|
|
61
|
+
# bites on: OpenAI treats it as the successor to `system`, so the
|
|
62
|
+
# OpenAI transforms express `instructions` as a developer message and
|
|
63
|
+
# any agent using them fails outright here.
|
|
64
|
+
#
|
|
65
|
+
# @param params [Hash] request parameters, mutated in place
|
|
66
|
+
# @return [void]
|
|
67
|
+
def demote_developer_roles!(params)
|
|
68
|
+
if params.key?(:instructions)
|
|
69
|
+
# One message per instruction to start. The request cleanup then
|
|
70
|
+
# merges consecutive same-role messages into a single message
|
|
71
|
+
# with content parts, which DeepSeek accepts — unlike `developer`,
|
|
72
|
+
# the part that actually fails.
|
|
73
|
+
params[:messages] = wrap(params.delete(:instructions)).map { |text| { role: "system", content: text } } +
|
|
74
|
+
wrap(params[:messages])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
messages = wrap(params[:messages])
|
|
78
|
+
return if messages.empty?
|
|
79
|
+
|
|
80
|
+
params[:messages] = messages.map do |message|
|
|
81
|
+
next message unless message.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
key = role_key(message)
|
|
84
|
+
next message unless key && message[key].to_s == "developer"
|
|
85
|
+
|
|
86
|
+
# A copy: the messages are the caller's own hashes — the provider
|
|
87
|
+
# keeps the very `context` the prompt was given — so rewriting one
|
|
88
|
+
# in place would rewrite it for every other provider that prompt
|
|
89
|
+
# reaches, and for every later reuse of the same context.
|
|
90
|
+
message.dup.tap { |copy| copy[key] = "system" }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Wraps a value that may be a single item, a list, or absent.
|
|
95
|
+
#
|
|
96
|
+
# `Array(hash)` splits a lone Hash into pairs, which is not what a
|
|
97
|
+
# single message or instruction means.
|
|
98
|
+
#
|
|
99
|
+
# @param value [Object, Array, nil]
|
|
100
|
+
# @return [Array]
|
|
101
|
+
def wrap(value)
|
|
102
|
+
return [] if value.nil?
|
|
103
|
+
return [ value ] if value.is_a?(Hash)
|
|
104
|
+
|
|
105
|
+
Array(value)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @param message [Hash]
|
|
109
|
+
# @return [Symbol, String, nil] whichever key the message spells its
|
|
110
|
+
# role with, since a caller may use either
|
|
111
|
+
def role_key(message)
|
|
112
|
+
return :role if message.key?(:role)
|
|
113
|
+
return "role" if message.key?("role")
|
|
114
|
+
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../open_ai/options"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module DeepSeek
|
|
8
|
+
# Configuration options for the DeepSeek provider.
|
|
9
|
+
#
|
|
10
|
+
# Extends OpenAI::Options because DeepSeek serves an OpenAI-compatible
|
|
11
|
+
# API. Only the endpoint and the credential's environment variable differ;
|
|
12
|
+
# DeepSeek has no organization or project scoping.
|
|
13
|
+
#
|
|
14
|
+
# @example Configuration in active_agent.yml
|
|
15
|
+
# deepseek:
|
|
16
|
+
# service: "DeepSeek"
|
|
17
|
+
# api_key: <%= ENV["DEEPSEEK_API_KEY"] %>
|
|
18
|
+
#
|
|
19
|
+
# @example Inline
|
|
20
|
+
# generate_with :deepseek, model: "deepseek-flash"
|
|
21
|
+
#
|
|
22
|
+
# @see OpenAI::Options
|
|
23
|
+
# @see https://api-docs.deepseek.com
|
|
24
|
+
class Options < OpenAI::Options
|
|
25
|
+
# DeepSeek's OpenAI-compatible endpoint. The Anthropic-compatible one
|
|
26
|
+
# lives at /anthropic and is served by AnthropicProvider instead.
|
|
27
|
+
DEFAULT_BASE_URL = "https://api.deepseek.com"
|
|
28
|
+
|
|
29
|
+
# @param kwargs [Hash]
|
|
30
|
+
# @option kwargs [String] :api_key
|
|
31
|
+
# @option kwargs [String] :base_url (https://api.deepseek.com)
|
|
32
|
+
def initialize(kwargs = {})
|
|
33
|
+
kwargs = kwargs.deep_symbolize_keys if kwargs.respond_to?(:deep_symbolize_keys)
|
|
34
|
+
|
|
35
|
+
super(kwargs.merge(base_url: kwargs[:base_url].presence || DEFAULT_BASE_URL))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def resolve_api_key(kwargs)
|
|
41
|
+
kwargs[:api_key] ||
|
|
42
|
+
kwargs[:access_token] ||
|
|
43
|
+
ENV["DEEPSEEK_API_KEY"] ||
|
|
44
|
+
ENV["DEEPSEEK_ACCESS_TOKEN"]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# DeepSeek is not an OpenAI organization; scoping one would be a lie.
|
|
48
|
+
def resolve_organization_id(_kwargs) = nil
|
|
49
|
+
def resolve_project_id(_kwargs) = nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require_relative "_base_provider"
|
|
2
|
+
|
|
3
|
+
require_gem!(:openai, __FILE__)
|
|
4
|
+
|
|
5
|
+
require_relative "open_ai_provider"
|
|
6
|
+
require_relative "deepseek/_types"
|
|
7
|
+
|
|
8
|
+
module ActiveAgent
|
|
9
|
+
module Providers
|
|
10
|
+
# Provides access to DeepSeek's OpenAI-compatible API.
|
|
11
|
+
#
|
|
12
|
+
# Extends OpenAI's Chat provider, which DeepSeek follows for request shape,
|
|
13
|
+
# response shape and tool calling. Thinking mode is left to DeepSeek's own
|
|
14
|
+
# default — see {DeepSeek::Chat::Request} — because the provider's API is the
|
|
15
|
+
# authority on how it wants to be called.
|
|
16
|
+
#
|
|
17
|
+
# @example Configuration in active_agent.yml
|
|
18
|
+
# deepseek:
|
|
19
|
+
# service: "DeepSeek"
|
|
20
|
+
# api_key: <%= ENV["DEEPSEEK_API_KEY"] %>
|
|
21
|
+
#
|
|
22
|
+
# @example Usage
|
|
23
|
+
# class TicketAgent < ApplicationAgent
|
|
24
|
+
# generate_with :deepseek, model: "deepseek-flash"
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# @see OpenAI::ChatProvider
|
|
28
|
+
# @see https://api-docs.deepseek.com
|
|
29
|
+
class DeepSeekProvider < OpenAI::ChatProvider
|
|
30
|
+
# @return [String]
|
|
31
|
+
def self.service_name
|
|
32
|
+
"DeepSeek"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Class]
|
|
36
|
+
def self.options_klass
|
|
37
|
+
namespace::Options
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [ActiveModel::Type::Value]
|
|
41
|
+
def self.prompt_request_type
|
|
42
|
+
namespace::Chat::RequestType.new
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -122,6 +122,19 @@ module ActiveAgent
|
|
|
122
122
|
end
|
|
123
123
|
# event_log_level :tool_call, :debug # Rails 8.1
|
|
124
124
|
|
|
125
|
+
# @param event [ActiveSupport::Notifications::Event]
|
|
126
|
+
# @return [void]
|
|
127
|
+
def input_requested(event)
|
|
128
|
+
trace_id = event.payload[:trace_id]
|
|
129
|
+
provider_module = event.payload[:provider_module]
|
|
130
|
+
tool_names = Array(event.payload[:input_requests]).map(&:tool_name)
|
|
131
|
+
|
|
132
|
+
debug do
|
|
133
|
+
"[#{trace_id}] [ActiveAgent] [#{provider_module}] Awaiting input for: #{tool_names.join(", ")}"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
# event_log_level :input_requested, :debug # Rails 8.1
|
|
137
|
+
|
|
125
138
|
# @param event [ActiveSupport::Notifications::Event]
|
|
126
139
|
# @return [void]
|
|
127
140
|
def stream_chunk(event)
|