activeagent 1.8.1 → 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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1058 -0
  3. data/README.md +9 -1
  4. data/lib/active_agent/base.rb +22 -1
  5. data/lib/active_agent/concerns/delegation.rb +4 -0
  6. data/lib/active_agent/concerns/input_requests.rb +178 -0
  7. data/lib/active_agent/concerns/parameterized.rb +7 -4
  8. data/lib/active_agent/concerns/provider.rb +1 -0
  9. data/lib/active_agent/concerns/tooling.rb +7 -2
  10. data/lib/active_agent/delegation/runner.rb +31 -0
  11. data/lib/active_agent/evals/catalog.rb +305 -0
  12. data/lib/active_agent/evals/ruby_llm.rb +411 -0
  13. data/lib/active_agent/evals/scenario.rb +10 -3
  14. data/lib/active_agent/evals/scenario_parser.rb +38 -10
  15. data/lib/active_agent/evals.rb +1 -0
  16. data/lib/active_agent/generation.rb +62 -2
  17. data/lib/active_agent/generation_job.rb +14 -3
  18. data/lib/active_agent/input_request/resume.rb +199 -0
  19. data/lib/active_agent/input_request.rb +279 -0
  20. data/lib/active_agent/providers/_base_provider.rb +153 -31
  21. data/lib/active_agent/providers/anthropic/transforms.rb +57 -28
  22. data/lib/active_agent/providers/anthropic_provider.rb +102 -20
  23. data/lib/active_agent/providers/common/responses/prompt.rb +21 -0
  24. data/lib/active_agent/providers/concerns/input_requests.rb +332 -0
  25. data/lib/active_agent/providers/concerns/mcp_serving.rb +267 -0
  26. data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +18 -3
  27. data/lib/active_agent/providers/deep_seek_provider.rb +6 -0
  28. data/lib/active_agent/providers/deepseek/_types.rb +4 -0
  29. data/lib/active_agent/providers/deepseek/chat/_types.rb +54 -0
  30. data/lib/active_agent/providers/deepseek/chat/request.rb +121 -0
  31. data/lib/active_agent/providers/deepseek/options.rb +53 -0
  32. data/lib/active_agent/providers/deepseek_provider.rb +46 -0
  33. data/lib/active_agent/providers/log_subscriber.rb +13 -0
  34. data/lib/active_agent/providers/mcp_bridge.rb +691 -0
  35. data/lib/active_agent/providers/mcp_tool_cache.rb +230 -0
  36. data/lib/active_agent/providers/open_ai/_base.rb +1 -1
  37. data/lib/active_agent/providers/open_ai/chat_provider.rb +22 -3
  38. data/lib/active_agent/providers/open_ai/options.rb +1 -1
  39. data/lib/active_agent/providers/open_ai/responses_provider.rb +68 -15
  40. data/lib/active_agent/providers/open_router/options.rb +6 -2
  41. data/lib/active_agent/providers/ruby_llm/messages/_types.rb +3 -1
  42. data/lib/active_agent/providers/ruby_llm/options.rb +5 -0
  43. data/lib/active_agent/providers/ruby_llm_provider.rb +127 -29
  44. data/lib/active_agent/schema_tools.rb +12 -0
  45. data/lib/active_agent/telemetry/instrumentation.rb +10 -1
  46. data/lib/active_agent/version.rb +1 -1
  47. data/lib/active_agent.rb +2 -0
  48. data/lib/generators/active_agent/schema_tools/USAGE +11 -0
  49. data/lib/generators/active_agent/schema_tools/schema_tools_generator.rb +39 -4
  50. data/lib/generators/active_agent/schema_tools/templates/schema_tools.rb.tt +13 -0
  51. metadata +31 -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)