ask-agent 0.3.1 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26c124febbf2652616754dd539cc8df5d24afe5fbe8c03381f760598578a051f
4
- data.tar.gz: 50d87794d60901a85da426f50a1a619807faf96bbe1fedb58145dd2d5d7fbeb1
3
+ metadata.gz: 3d59df39cdd8e4f3aef8b71f9c63fee0c571735d03f409667d3c3b9fed964e59
4
+ data.tar.gz: e6546221a9c0d609d6b0bcaa4295d6c285ebf6e9e2750e2ee69715cda257017c
5
5
  SHA512:
6
- metadata.gz: 5927098dcbd1fe84faa0cf2a7cddbc63e544a7113e47c61f972044e598053758417edce0e63d9a55d7922fbbc65ad9f552ef5c3fb1c07ac71ba1b718d1cbb815
7
- data.tar.gz: 0436ac54f1dcd57ac06706d7e6a8ad5030574341170e0c7e99ca174d3dd94341651142c78bf5d138be1f99a978d71eb21f03a57df1ef4b6cb038ebb22778a637
6
+ metadata.gz: f0569cd462c103a755d1a05606150ef229504cc090a8b0dfbbd1bfb79dba8f67cac82d1a1aaa1039ce75294b3aaf55d9f68478df5de1f878b197f5bc6d12335a
7
+ data.tar.gz: 970cabb7492914f0874a76c04ed72f04d9882020a900db3a8d07ca043097432a4e25bfa2c46e3678d7db67cd98f8d25a03d97c64dab1157a2278f97bb23132cd
data/CHANGELOG.md CHANGED
@@ -1,3 +1,33 @@
1
+ ## [0.4.0] — 2026-07-17
2
+
3
+ ### Added
4
+
5
+ - **Agent testing framework** — `Ask::Agent::Test` provides deterministic agent behavior tests without calling real LLMs. Stub tool calls and text responses, assert which tools were called, in what order, and verify the final response. No flaky tests, no API keys, no cost.
6
+
7
+ ```ruby
8
+ require "ask/agent/test"
9
+
10
+ class MyAgentTest < Minitest::Test
11
+ include Ask::Agent::Test::Assertions
12
+
13
+ def setup
14
+ @session = Ask::Agent::Session.new(model: "gpt-4o", tools: [my_tool])
15
+ @session.test_mode
16
+ end
17
+
18
+ def test_calls_search_tool
19
+ @session.stub_tool_call("search", query: "weather")
20
+ @session.stub_text("Sunny")
21
+ @session.run("What's the weather?")
22
+ assert_called_tool "search"
23
+ assert_final_response /Sunny/
24
+ assert_no_unused_stubs
25
+ end
26
+ end
27
+ ```
28
+
29
+ Assertions: `assert_called_tool`, `refute_called_tool`, `assert_tool_order`, `assert_final_response`, `assert_no_unused_stubs`.
30
+
1
31
  ## [0.3.1] — 2026-07-17
2
32
 
3
33
  ### Added
@@ -51,7 +51,7 @@ module Ask
51
51
 
52
52
  result = chat_with_retry(stream, calls_acc, &block)
53
53
 
54
- response_msg = if stream
54
+ response_msg = if result.respond_to?(:chunks)
55
55
  build_stream_response(result, calls_acc)
56
56
  else
57
57
  build_response(result)
@@ -104,10 +104,12 @@ module Ask
104
104
  @messages.clear
105
105
  end
106
106
 
107
+ attr_writer :test_provider
108
+
107
109
  private
108
110
 
109
111
  def provider
110
- @provider ||= build_provider
112
+ @test_provider || @provider ||= build_provider
111
113
  end
112
114
 
113
115
  def build_provider
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Agent
5
+ module Test
6
+ # Holds stubs, tracks called tools and final response for a test session.
7
+ class Mode
8
+ attr_reader :stubs, :called_tools, :final_response
9
+
10
+ def initialize
11
+ @stubs = []
12
+ @called_tools = []
13
+ @final_response = nil
14
+ end
15
+
16
+ def stub_tool_call(name, arguments = {})
17
+ @stubs << { type: :tool_call, name: name.to_s, arguments: arguments }
18
+ end
19
+
20
+ def stub_text(content)
21
+ @stubs << { type: :text, content: content }
22
+ end
23
+
24
+ def record_tool_call(name)
25
+ @called_tools << name
26
+ end
27
+
28
+ def record_final_response(response)
29
+ @final_response = response
30
+ end
31
+
32
+ def unused_stubs
33
+ @stubs
34
+ end
35
+ end
36
+
37
+ # Stub provider that returns canned responses instead of calling an LLM.
38
+ class StubProvider
39
+ def initialize(mode)
40
+ @mode = mode
41
+ end
42
+
43
+ def chat(messages, model: nil, tools: nil, temperature: nil, stream: nil, schema: nil, **params)
44
+ stub = @mode.stubs.shift or raise "No more stubs available. Unconsumed messages: #{messages.length}"
45
+
46
+ case stub[:type]
47
+ when :tool_call
48
+ Ask::Message.new(
49
+ role: :assistant,
50
+ content: nil,
51
+ tool_calls: [{
52
+ id: "call_test_#{SecureRandom.hex(4)}",
53
+ type: "function",
54
+ name: stub[:name],
55
+ arguments: JSON.generate(stub[:arguments])
56
+ }],
57
+ metadata: { input_tokens: 10, output_tokens: 5 }
58
+ )
59
+ when :text
60
+ Ask::Message.new(
61
+ role: :assistant,
62
+ content: stub[:content],
63
+ metadata: { input_tokens: 10, output_tokens: 5 }
64
+ )
65
+ else
66
+ raise "Unknown stub type: #{stub[:type].inspect}"
67
+ end
68
+ end
69
+
70
+ def headers
71
+ {}
72
+ end
73
+ end
74
+
75
+ # Methods added to Session when in test mode.
76
+ module SessionOverride
77
+ def test_mode
78
+ return @test_mode if @test_mode
79
+
80
+ @test_mode = Mode.new
81
+ @chat&.test_provider = StubProvider.new(@test_mode) if @chat
82
+ @test_mode
83
+ end
84
+
85
+ def stub_tool_call(name, arguments = {})
86
+ test_mode.stub_tool_call(name, arguments)
87
+ end
88
+
89
+ def stub_text(content)
90
+ test_mode.stub_text(content)
91
+ end
92
+
93
+ def called_tool?(name)
94
+ test_mode.called_tools.include?(name.to_s)
95
+ end
96
+
97
+ private
98
+
99
+ def build_chat(model, system_prompt, tools, **chat_options)
100
+ if @test_mode
101
+ chat = Ask::Agent::Chat.new(model: model, tools: tools, **chat_options)
102
+ chat.with_instructions(system_prompt) if system_prompt
103
+ chat.test_provider = StubProvider.new(@test_mode)
104
+ chat
105
+ else
106
+ super
107
+ end
108
+ end
109
+
110
+ def emit(event)
111
+ super
112
+ return unless @test_mode
113
+
114
+ case event
115
+ when Ask::Agent::Events::ToolExecutionStart
116
+ @test_mode.record_tool_call(event.name)
117
+ when Ask::Agent::Events::SessionEnd
118
+ @test_mode.record_final_response(event.result)
119
+ end
120
+ end
121
+
122
+ public :emit
123
+ end
124
+
125
+ # Test assertions to include in Minitest test classes.
126
+ module Assertions
127
+ def session
128
+ @session || raise("@session not set — define it in setup")
129
+ end
130
+
131
+ def assert_called_tool(name, msg = nil)
132
+ assert session.called_tool?(name),
133
+ msg || "Expected tool #{name.inspect} to be called. Called: #{session.test_mode.called_tools.inspect}"
134
+ end
135
+
136
+ def refute_called_tool(name, msg = nil)
137
+ refute session.called_tool?(name),
138
+ msg || "Expected tool #{name.inspect} not to be called. Called: #{session.test_mode.called_tools.inspect}"
139
+ end
140
+
141
+ def assert_tool_order(names, msg = nil)
142
+ expected = names.map(&:to_s)
143
+ called = session.test_mode.called_tools
144
+ matched = called.first(expected.length)
145
+ assert_equal expected, matched,
146
+ msg || "Tool call order mismatch. Expected: #{expected.inspect}, Got: #{matched.inspect}"
147
+ end
148
+
149
+ def assert_final_response(match, msg = nil)
150
+ assert_match match, session.test_mode.final_response.to_s, msg
151
+ end
152
+
153
+ def assert_no_unused_stubs(msg = nil)
154
+ assert_empty session.test_mode.unused_stubs,
155
+ msg || "Unused stubs: #{session.test_mode.unused_stubs.inspect}"
156
+ end
157
+ end
158
+
159
+ Ask::Agent::Session.prepend(SessionOverride)
160
+ end
161
+ end
162
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.3.1"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
data/lib/ask/agent.rb CHANGED
@@ -56,3 +56,6 @@ require_relative "agent/configuration"
56
56
  require_relative "agent/meta_agent"
57
57
  require_relative "agent/persistence/base"
58
58
  require_relative "agent/persistence/in_memory"
59
+
60
+ # Test helpers (loaded on demand)
61
+ autoload :Test, "ask/agent/test"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -149,6 +149,7 @@ files:
149
149
  - lib/ask/agent/reflector.rb
150
150
  - lib/ask/agent/session.rb
151
151
  - lib/ask/agent/telemetry.rb
152
+ - lib/ask/agent/test.rb
152
153
  - lib/ask/agent/tool_abort_controller.rb
153
154
  - lib/ask/agent/tool_executor.rb
154
155
  - lib/ask/agent/version.rb