ask-agent 0.1.0 → 0.1.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.
- checksums.yaml +4 -4
- data/lib/ask/agent/chat.rb +221 -0
- data/lib/ask/agent/compactor.rb +2 -2
- data/lib/ask/agent/meta_agent.rb +1 -1
- data/lib/ask/agent/reflector.rb +2 -2
- data/lib/ask/agent/session.rb +2 -12
- data/lib/ask/agent/version.rb +1 -1
- data/lib/ask/agent.rb +1 -1
- metadata +24 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e875e155f72f3bc773ff218b64243ac918354fac930b0181e02ae1a74d0bc4eb
|
|
4
|
+
data.tar.gz: 9ec62c98c64b7307f445311cce0f6fd6bf81a9f7fde52daa505cd532d4734520
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 14d6490ab16f5e675e05ac3e72f37c113edcfd7e7339c10fe6a73290b88e4cab0ab8535d21860e51d09aac36c7a7066b3dbfe1f19b04d5c873a301094fb610fe
|
|
7
|
+
data.tar.gz: 729b178fb7f474e2fe5c84ce692169ba184fefe1b761d7ddc2a1dfaf39c57d184155f9f67e186bd91b13faaab05a5e79aac49107055d0089269ca10e9e100f94
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module Agent
|
|
5
|
+
# Response message returned by {Chat#ask}.
|
|
6
|
+
# Presents a message-like response interface for ask-agent internal use.
|
|
7
|
+
ResponseMessage = Data.define(:content, :tool_calls, :thinking) do
|
|
8
|
+
def tool_call? = !tool_calls.empty?
|
|
9
|
+
def to_s = content.to_s
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Tool call data used in {ResponseMessage} and {ChatChunk}.
|
|
13
|
+
ToolCallInfo = Data.define(:id, :name, :arguments)
|
|
14
|
+
|
|
15
|
+
# Chunk yielded during streaming from {Chat#ask}.
|
|
16
|
+
ChatChunk = Data.define(:content, :tool_calls, :thinking) do
|
|
17
|
+
def tool_call? = !tool_calls.empty?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Thin wrapper around {Ask::Provider} + an internal message array that
|
|
21
|
+
# presents a Chat-like API for ask-agent internal use.
|
|
22
|
+
#
|
|
23
|
+
# Manages conversation history, resolves the correct provider/model,
|
|
24
|
+
# handles streaming chunk accumulation, and normalises tool call
|
|
25
|
+
# formats between Ask::Provider (Array of Hashes) and ask-agent
|
|
26
|
+
# internal usage (Hash of { id => ToolCallInfo }).
|
|
27
|
+
class Chat
|
|
28
|
+
# @return [String] model ID (e.g. "gpt-4o")
|
|
29
|
+
attr_reader :model
|
|
30
|
+
|
|
31
|
+
# @return [Array<Ask::Message>] all messages in the conversation
|
|
32
|
+
attr_reader :messages
|
|
33
|
+
|
|
34
|
+
# @param model [String, #ask] model ID or chat-like object
|
|
35
|
+
# @param tools [Array<Ask::Tool>] tool instances available to the chat
|
|
36
|
+
# @param temperature [Float, nil] sampling temperature
|
|
37
|
+
# @param schema [Ask::Schema, Hash, nil] structured output schema
|
|
38
|
+
def initialize(model:, tools: [], temperature: nil, schema: nil, **)
|
|
39
|
+
@model_id = model.respond_to?(:id) ? model.id : model.to_s
|
|
40
|
+
@model_info = resolve_model(@model_id)
|
|
41
|
+
@tools = tools
|
|
42
|
+
@temperature = temperature
|
|
43
|
+
@schema = schema
|
|
44
|
+
@messages = []
|
|
45
|
+
@provider = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Send a user message and get a completion response.
|
|
49
|
+
#
|
|
50
|
+
# @param message [String, nil] user message text
|
|
51
|
+
# @yield [ChatChunk] streaming chunks (only when a block is given)
|
|
52
|
+
# @return [ResponseMessage] the assistant's response
|
|
53
|
+
def ask(message = nil, &block)
|
|
54
|
+
@messages << Ask::Message.new(role: :user, content: message.to_s) if message
|
|
55
|
+
|
|
56
|
+
stream = block_given?
|
|
57
|
+
tool_defs = @tools.map { |t| Ask::ToolDef.from_tool(t) }
|
|
58
|
+
|
|
59
|
+
# Accumulator for tool calls during streaming (keyed by index)
|
|
60
|
+
calls_acc = {}
|
|
61
|
+
|
|
62
|
+
result = provider.chat(
|
|
63
|
+
@messages.map(&:to_h),
|
|
64
|
+
model: @model_id,
|
|
65
|
+
tools: tool_defs,
|
|
66
|
+
temperature: @temperature,
|
|
67
|
+
stream: stream,
|
|
68
|
+
schema: @schema&.respond_to?(:to_json_schema) ? @schema.to_json_schema : @schema
|
|
69
|
+
) do |raw_chunk|
|
|
70
|
+
next unless block_given?
|
|
71
|
+
|
|
72
|
+
# Accumulate tool calls by index during streaming
|
|
73
|
+
accumulate_tool_calls(raw_chunk, calls_acc)
|
|
74
|
+
|
|
75
|
+
# Yield adapted chunk with current tool call state
|
|
76
|
+
yield ChatChunk.new(
|
|
77
|
+
content: raw_chunk.content,
|
|
78
|
+
tool_calls: build_current_tool_calls(calls_acc),
|
|
79
|
+
thinking: raw_chunk.respond_to?(:thinking) ? raw_chunk.thinking : nil
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
response_msg = if stream
|
|
84
|
+
build_stream_response(result, calls_acc)
|
|
85
|
+
else
|
|
86
|
+
build_response(result)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Store assistant response in conversation history
|
|
90
|
+
@messages << Ask::Message.new(
|
|
91
|
+
role: :assistant,
|
|
92
|
+
content: response_msg.content,
|
|
93
|
+
tool_calls: response_msg.tool_calls&.values&.map { |tc|
|
|
94
|
+
{ id: tc.id, type: "function", name: tc.name, arguments: tc.arguments }
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
response_msg
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Add a message to the conversation history.
|
|
102
|
+
#
|
|
103
|
+
# @param role [Symbol] :system, :user, :assistant, :tool
|
|
104
|
+
# @param content [String, nil] message content
|
|
105
|
+
# @param tool_call_id [String, nil] tool call ID (for tool results)
|
|
106
|
+
# @param tool_calls [Array<Hash>, nil] tool call invocations
|
|
107
|
+
def add_message(role:, content: nil, tool_call_id: nil, tool_calls: nil)
|
|
108
|
+
@messages << Ask::Message.new(
|
|
109
|
+
role: role,
|
|
110
|
+
content: content,
|
|
111
|
+
tool_call_id: tool_call_id,
|
|
112
|
+
tool_calls: tool_calls
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Set or replace the system prompt.
|
|
117
|
+
#
|
|
118
|
+
# @param prompt [String] system instructions
|
|
119
|
+
# @return [self]
|
|
120
|
+
def with_instructions(prompt)
|
|
121
|
+
@messages.reject! { |m| m.role == :system }
|
|
122
|
+
@messages.unshift(Ask::Message.new(role: :system, content: prompt))
|
|
123
|
+
self
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Clear all messages from the conversation.
|
|
127
|
+
def reset_messages!
|
|
128
|
+
@messages.clear
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
# Resolve model info from the catalog.
|
|
134
|
+
def resolve_model(model_id)
|
|
135
|
+
Ask::ModelCatalog.find(model_id)
|
|
136
|
+
rescue Ask::ModelNotFound
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Lazily resolve and instantiate the LLM provider.
|
|
141
|
+
def provider
|
|
142
|
+
@provider ||= build_provider
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def build_provider
|
|
146
|
+
slug = @model_info&.provider || "openai"
|
|
147
|
+
klass = Ask::Provider.resolve(slug)
|
|
148
|
+
klass.new(provider_config(slug))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def provider_config(slug, extra_keys: {})
|
|
152
|
+
env_key = "#{slug.upcase}_API_KEY"
|
|
153
|
+
key = ENV[env_key] || ENV["OPENCODE_API_KEY"] || ENV["OPENAI_API_KEY"]
|
|
154
|
+
base = ENV["#{slug.upcase}_API_BASE"] || ENV["OPENCODE_API_BASE"]
|
|
155
|
+
config = { api_key: key }
|
|
156
|
+
config[:"#{slug}_api_key"] = key
|
|
157
|
+
config[:"#{slug}_api_base"] = base if base
|
|
158
|
+
Ask::LLM::Config.new(config)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Accumulate partial tool calls from streaming chunks.
|
|
162
|
+
def accumulate_tool_calls(raw_chunk, calls_acc)
|
|
163
|
+
return unless raw_chunk.tool_call?
|
|
164
|
+
|
|
165
|
+
raw_chunk.tool_calls.each do |tc|
|
|
166
|
+
idx = tc[:index] || 0
|
|
167
|
+
calls_acc[idx] ||= { id: tc[:id], name: tc[:name], arguments: +"" }
|
|
168
|
+
calls_acc[idx][:id] ||= tc[:id]
|
|
169
|
+
calls_acc[idx][:name] ||= tc[:name]
|
|
170
|
+
calls_acc[idx][:arguments] << tc[:arguments].to_s if tc[:arguments]
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Build current snapshot of tool calls from accumulator.
|
|
175
|
+
def build_current_tool_calls(calls_acc)
|
|
176
|
+
hash = {}
|
|
177
|
+
calls_acc.each_value do |tc_data|
|
|
178
|
+
next unless tc_data[:id]
|
|
179
|
+
hash[tc_data[:id]] = ToolCallInfo.new(
|
|
180
|
+
id: tc_data[:id],
|
|
181
|
+
name: tc_data[:name] || "",
|
|
182
|
+
arguments: tc_data[:arguments]
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
hash
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Convert Ask::Provider tool_calls (Array of Hashes) to Hash.
|
|
189
|
+
def build_tool_call_hash(raw_calls)
|
|
190
|
+
hash = {}
|
|
191
|
+
raw_calls.each do |tc|
|
|
192
|
+
id = tc[:id] || tc["id"]
|
|
193
|
+
next unless id
|
|
194
|
+
hash[id] = ToolCallInfo.new(
|
|
195
|
+
id: id,
|
|
196
|
+
name: tc[:name] || tc["name"] || "",
|
|
197
|
+
arguments: tc[:arguments] || tc["arguments"] || ""
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
hash
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Build response from streaming result.
|
|
204
|
+
def build_stream_response(stream, calls_acc)
|
|
205
|
+
thinking = stream.chunks.filter_map(&:thinking).last
|
|
206
|
+
ResponseMessage.new(
|
|
207
|
+
content: stream.accumulated_text,
|
|
208
|
+
tool_calls: build_current_tool_calls(calls_acc),
|
|
209
|
+
thinking: thinking
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Build response from non-streaming result.
|
|
214
|
+
def build_response(msg)
|
|
215
|
+
tool_calls = msg.tool_calls ? build_tool_call_hash(msg.tool_calls) : {}
|
|
216
|
+
thinking = msg.respond_to?(:thinking) ? msg.thinking : nil
|
|
217
|
+
ResponseMessage.new(content: msg.content.to_s, tool_calls: tool_calls, thinking: thinking)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
data/lib/ask/agent/compactor.rb
CHANGED
|
@@ -138,8 +138,8 @@ module Ask
|
|
|
138
138
|
|
|
139
139
|
def build_llm_chat
|
|
140
140
|
if @llm.respond_to?(:ask) then @llm
|
|
141
|
-
elsif @llm.is_a?(String) then
|
|
142
|
-
else
|
|
141
|
+
elsif @llm.is_a?(String) then Ask::Agent::Chat.new(model: @llm)
|
|
142
|
+
else Ask::Agent::Chat.new(model: Ask::Agent.configuration.default_model) end
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
def extract_summary
|
data/lib/ask/agent/meta_agent.rb
CHANGED
|
@@ -100,7 +100,7 @@ module Ask
|
|
|
100
100
|
|
|
101
101
|
def call_llm_for_analysis(telemetry_data, existing_recommendations, resolved_ids)
|
|
102
102
|
prompt = build_analysis_prompt(telemetry_data, existing_recommendations, resolved_ids)
|
|
103
|
-
chat =
|
|
103
|
+
chat = Ask::Agent::Chat.new(model: @model, **@chat_options)
|
|
104
104
|
response = chat.ask(prompt)
|
|
105
105
|
parse_llm_response(response.content.to_s)
|
|
106
106
|
rescue => e
|
data/lib/ask/agent/reflector.rb
CHANGED
|
@@ -66,12 +66,12 @@ module Ask
|
|
|
66
66
|
|
|
67
67
|
def build_eval_chat
|
|
68
68
|
model_id = model_id_from(@model)
|
|
69
|
-
|
|
69
|
+
Ask::Agent::Chat.new(model: model_id)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def model_id_from(model)
|
|
73
73
|
case model
|
|
74
|
-
when
|
|
74
|
+
when Ask::Agent::Chat then model.model.to_s
|
|
75
75
|
when String then model
|
|
76
76
|
else model.to_s
|
|
77
77
|
end
|
data/lib/ask/agent/session.rb
CHANGED
|
@@ -35,7 +35,6 @@ module Ask
|
|
|
35
35
|
|
|
36
36
|
@chat = build_chat(model, system_prompt, tools, **chat_options)
|
|
37
37
|
@tools = resolve_tools(tools)
|
|
38
|
-
register_tools_on_chat
|
|
39
38
|
@loop = Loop.new(max_turns: max_turns)
|
|
40
39
|
@tool_executor = ToolExecutor.new(max_retries: max_tool_retries, parallel: parallel_tools)
|
|
41
40
|
@compactor = compactor ? build_compactor(compactor) : nil
|
|
@@ -96,7 +95,7 @@ module Ask
|
|
|
96
95
|
emit(Events::LoopDetected.new(tool_name: e.message, repeated_count: 3))
|
|
97
96
|
@telemetry.log(:loop_detected, session_id: @id, tool_name: e.message, repeated_count: 3)
|
|
98
97
|
response = last_content
|
|
99
|
-
rescue
|
|
98
|
+
rescue Ask::ContextLengthExceeded
|
|
100
99
|
if @compactor && !@compactor.overflow_recovered?
|
|
101
100
|
@compactor.recover_from_overflow
|
|
102
101
|
retry
|
|
@@ -210,20 +209,11 @@ module Ask
|
|
|
210
209
|
|
|
211
210
|
private
|
|
212
211
|
|
|
213
|
-
def register_tools_on_chat
|
|
214
|
-
return unless @tools.any?
|
|
215
|
-
|
|
216
|
-
def @chat.handle_tool_calls(response, &)
|
|
217
|
-
@on[:end_message]&.call(response) if @on[:end_message]
|
|
218
|
-
response
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
|
|
222
212
|
def build_chat(model, system_prompt, tools, **chat_options)
|
|
223
213
|
if model.respond_to?(:ask)
|
|
224
214
|
model
|
|
225
215
|
else
|
|
226
|
-
chat =
|
|
216
|
+
chat = Ask::Agent::Chat.new(model: model, tools: tools, **chat_options)
|
|
227
217
|
chat.with_instructions(system_prompt) if system_prompt
|
|
228
218
|
chat
|
|
229
219
|
end
|
data/lib/ask/agent/version.rb
CHANGED
data/lib/ask/agent.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "ruby_llm"
|
|
4
3
|
require "fileutils"
|
|
5
4
|
require "json"
|
|
6
5
|
require "securerandom"
|
|
@@ -41,6 +40,7 @@ end
|
|
|
41
40
|
|
|
42
41
|
require_relative "agent/version"
|
|
43
42
|
require_relative "agent/events"
|
|
43
|
+
require_relative "agent/chat"
|
|
44
44
|
require_relative "agent/telemetry"
|
|
45
45
|
require_relative "agent/tool_abort_controller"
|
|
46
46
|
require_relative "agent/session"
|
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.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -10,7 +10,7 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: ask-
|
|
13
|
+
name: ask-core
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
@@ -24,7 +24,7 @@ dependencies:
|
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0.1'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: ask-
|
|
27
|
+
name: ask-llm-providers
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
@@ -38,19 +38,33 @@ dependencies:
|
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0.1'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
|
-
name:
|
|
41
|
+
name: ask-tools
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
|
-
- - "
|
|
44
|
+
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '1
|
|
47
|
-
type: :
|
|
46
|
+
version: '0.1'
|
|
47
|
+
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
|
-
- - "
|
|
51
|
+
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '1
|
|
53
|
+
version: '0.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: ask-tools-shell
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.1'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.1'
|
|
54
68
|
- !ruby/object:Gem::Dependency
|
|
55
69
|
name: minitest
|
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -105,6 +119,7 @@ files:
|
|
|
105
119
|
- README.md
|
|
106
120
|
- lib/ask-agent.rb
|
|
107
121
|
- lib/ask/agent.rb
|
|
122
|
+
- lib/ask/agent/chat.rb
|
|
108
123
|
- lib/ask/agent/compactor.rb
|
|
109
124
|
- lib/ask/agent/configuration.rb
|
|
110
125
|
- lib/ask/agent/events.rb
|