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 +4 -4
- data/CHANGELOG.md +30 -0
- data/lib/ask/agent/chat.rb +4 -2
- data/lib/ask/agent/test.rb +162 -0
- data/lib/ask/agent/version.rb +1 -1
- data/lib/ask/agent.rb +3 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d59df39cdd8e4f3aef8b71f9c63fee0c571735d03f409667d3c3b9fed964e59
|
|
4
|
+
data.tar.gz: e6546221a9c0d609d6b0bcaa4295d6c285ebf6e9e2750e2ee69715cda257017c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/ask/agent/chat.rb
CHANGED
|
@@ -51,7 +51,7 @@ module Ask
|
|
|
51
51
|
|
|
52
52
|
result = chat_with_retry(stream, calls_acc, &block)
|
|
53
53
|
|
|
54
|
-
response_msg = if
|
|
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
|
data/lib/ask/agent/version.rb
CHANGED
data/lib/ask/agent.rb
CHANGED
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.
|
|
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
|