ask-agent 0.1.0 → 0.1.2
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 +36 -12
- data/lib/ask/agent/session_backup.rb +237 -0
- data/lib/ask/agent/version.rb +1 -1
- data/lib/ask/agent.rb +2 -1
- metadata +38 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc1ea1b84a7139944acf9787cfdd3e5a9fad739b759fc4501f59f68bdd3c0b77
|
|
4
|
+
data.tar.gz: 37733f0982e0d991e284d46c9202fe5846fff67a433acae0385a703912c7f898
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e4ff593f7856c2fe0da46dfe8091ad38217e29b27aa343e95f6f939ab741b8c249217dbaa397ad609c21b9b4f39e09a781738a556368e69307fa01329fb00d9
|
|
7
|
+
data.tar.gz: fc45d3ae62bc2e325a22dedab88625f312d3e9b07188d9614c5cb2ddfa9683ed31e2bb8473f10344d73ac18f7cef202fcf53cd68272affbab54c208770017dde
|
|
@@ -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
|
@@ -14,6 +14,8 @@ module Ask
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
attr_reader :meta_agent_results
|
|
17
|
+
# @return [Ask::Skills::Registry, nil] auto-discovered skills registry
|
|
18
|
+
attr_reader :skills_registry
|
|
17
19
|
|
|
18
20
|
def initialize(model:, tools: [], max_turns: 25, max_tool_retries: 3,
|
|
19
21
|
compactor: nil, hooks: {}, persistence: nil,
|
|
@@ -35,11 +37,20 @@ module Ask
|
|
|
35
37
|
|
|
36
38
|
@chat = build_chat(model, system_prompt, tools, **chat_options)
|
|
37
39
|
@tools = resolve_tools(tools)
|
|
38
|
-
register_tools_on_chat
|
|
39
40
|
@loop = Loop.new(max_turns: max_turns)
|
|
40
41
|
@tool_executor = ToolExecutor.new(max_retries: max_tool_retries, parallel: parallel_tools)
|
|
41
42
|
@compactor = compactor ? build_compactor(compactor) : nil
|
|
42
43
|
@hooks = Hooks.new(hooks)
|
|
44
|
+
|
|
45
|
+
# Auto-discover skills and inject into system prompt
|
|
46
|
+
@skills_registry = Ask::Skills.discover rescue nil
|
|
47
|
+
if @skills_registry && !@skills_registry.names.empty?
|
|
48
|
+
skill_text = @skills_registry.format_for_prompt
|
|
49
|
+
if !skill_text.empty? && @chat.messages.any? { |m| m.role == :system }
|
|
50
|
+
current = @chat.messages.find { |m| m.role == :system }.content.to_s
|
|
51
|
+
@chat.with_instructions(current + skill_text)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
43
54
|
@persistence = persistence
|
|
44
55
|
|
|
45
56
|
reflector_opts = reflector.is_a?(Hash) ? reflector : {}
|
|
@@ -96,7 +107,7 @@ module Ask
|
|
|
96
107
|
emit(Events::LoopDetected.new(tool_name: e.message, repeated_count: 3))
|
|
97
108
|
@telemetry.log(:loop_detected, session_id: @id, tool_name: e.message, repeated_count: 3)
|
|
98
109
|
response = last_content
|
|
99
|
-
rescue
|
|
110
|
+
rescue Ask::ContextLengthExceeded
|
|
100
111
|
if @compactor && !@compactor.overflow_recovered?
|
|
101
112
|
@compactor.recover_from_overflow
|
|
102
113
|
retry
|
|
@@ -203,6 +214,28 @@ module Ask
|
|
|
203
214
|
|
|
204
215
|
def abort_requested? = @abort_requested
|
|
205
216
|
|
|
217
|
+
# Load a skill by name or file path.
|
|
218
|
+
# Injects the skill's full instructions into the conversation as a system message.
|
|
219
|
+
#
|
|
220
|
+
# @param name [String] skill name (e.g. "rails.db_debug") or path to a .md file
|
|
221
|
+
# @raise [Ask::Skills::Error] if the skill is not found
|
|
222
|
+
def skill(name)
|
|
223
|
+
if @skills_registry && (s = @skills_registry[name])
|
|
224
|
+
@chat.add_message(
|
|
225
|
+
role: :system,
|
|
226
|
+
content: "## Skill: #{s.name}\n\n#{s.description}\n\n---\n\n#{s.instructions}"
|
|
227
|
+
)
|
|
228
|
+
elsif File.exist?(name.to_s)
|
|
229
|
+
content = File.read(name.to_s)
|
|
230
|
+
@chat.add_message(
|
|
231
|
+
role: :system,
|
|
232
|
+
content: "## Skill: #{name}\n\n---\n\n#{content}"
|
|
233
|
+
)
|
|
234
|
+
else
|
|
235
|
+
raise Ask::Skills::Error, "Skill not found: #{name.inspect}"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
206
239
|
def reset_messages!
|
|
207
240
|
@chat.reset_messages!
|
|
208
241
|
@messages = []
|
|
@@ -210,20 +243,11 @@ module Ask
|
|
|
210
243
|
|
|
211
244
|
private
|
|
212
245
|
|
|
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
246
|
def build_chat(model, system_prompt, tools, **chat_options)
|
|
223
247
|
if model.respond_to?(:ask)
|
|
224
248
|
model
|
|
225
249
|
else
|
|
226
|
-
chat =
|
|
250
|
+
chat = Ask::Agent::Chat.new(model: model, tools: tools, **chat_options)
|
|
227
251
|
chat.with_instructions(system_prompt) if system_prompt
|
|
228
252
|
chat
|
|
229
253
|
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Ask
|
|
7
|
+
module Agent
|
|
8
|
+
class Session
|
|
9
|
+
attr_reader :id, :chat, :tools, :turn_count, :created_at, :messages
|
|
10
|
+
attr_reader :tool_calls_made
|
|
11
|
+
|
|
12
|
+
def reflection_count
|
|
13
|
+
@reflector&.reflection_count || 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# @return [Ask::Skills::Registry, nil] auto-discovered skills registry
|
|
18
|
+
attr_reader :skills_registry
|
|
19
|
+
attr_reader :meta_agent_results
|
|
20
|
+
# @return [Ask::Skills::Registry, nil] auto-discovered skills registry
|
|
21
|
+
attr_reader :skills_registry
|
|
22
|
+
|
|
23
|
+
def initialize(model:, tools: [], max_turns: 25, max_tool_retries: 3,
|
|
24
|
+
compactor: nil, hooks: {}, persistence: nil,
|
|
25
|
+
id: nil, system_prompt: nil, parallel_tools: true,
|
|
26
|
+
reflector: nil, telemetry: true, meta_agent: nil, **chat_options)
|
|
27
|
+
@id = id || SecureRandom.uuid
|
|
28
|
+
@max_turns = max_turns
|
|
29
|
+
@max_tool_retries = max_tool_retries
|
|
30
|
+
@parallel_tools = parallel_tools
|
|
31
|
+
@event_handlers = { all: [] }
|
|
32
|
+
@running = false
|
|
33
|
+
@deleted = false
|
|
34
|
+
@abort_requested = false
|
|
35
|
+
@turn_count = 0
|
|
36
|
+
@created_at = Time.now
|
|
37
|
+
@_no_tools_instructed = false
|
|
38
|
+
|
|
39
|
+
@telemetry = telemetry.is_a?(Telemetry) ? telemetry : Telemetry.new(enabled: !!telemetry)
|
|
40
|
+
|
|
41
|
+
@chat = build_chat(model, system_prompt, tools, **chat_options)
|
|
42
|
+
@tools = resolve_tools(tools)
|
|
43
|
+
@loop = Loop.new(max_turns: max_turns)
|
|
44
|
+
@tool_executor = ToolExecutor.new(max_retries: max_tool_retries, parallel: parallel_tools)
|
|
45
|
+
|
|
46
|
+
# Auto-discover skills and inject into system prompt
|
|
47
|
+
@skills_registry = Ask::Skills.discover rescue nil
|
|
48
|
+
if @skills_registry && !@skills_registry.names.empty?
|
|
49
|
+
skill_text = @skills_registry.format_for_prompt
|
|
50
|
+
if !skill_text.empty? && @chat.messages.any? { |m| m.role == :system }
|
|
51
|
+
current = @chat.messages.find { |m| m.role == :system }.content.to_s
|
|
52
|
+
@chat.with_instructions(current + skill_text)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
@compactor = compactor ? build_compactor(compactor) : nil
|
|
56
|
+
@hooks = Hooks.new(hooks)
|
|
57
|
+
|
|
58
|
+
# Auto-discover skills and inject into system prompt
|
|
59
|
+
@skills_registry = Ask::Skills.discover rescue nil
|
|
60
|
+
if @skills_registry && !@skills_registry.names.empty?
|
|
61
|
+
skill_text = @skills_registry.format_for_prompt
|
|
62
|
+
if !skill_text.empty? && @chat.messages.any? { |m| m.role == :system }
|
|
63
|
+
current = @chat.messages.find { |m| m.role == :system }.content.to_s
|
|
64
|
+
@chat.with_instructions(current + skill_text)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
@persistence = persistence
|
|
68
|
+
|
|
69
|
+
reflector_opts = reflector.is_a?(Hash) ? reflector : {}
|
|
70
|
+
@reflector = if reflector
|
|
71
|
+
Reflector.new(
|
|
72
|
+
model: @chat,
|
|
73
|
+
max_reflections: reflector_opts[:max_reflections] || 1
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
@meta_agent_config = meta_agent
|
|
78
|
+
@meta_agent_results = nil
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Auto-discover skills and inject into system prompt
|
|
82
|
+
@skills_registry = Ask::Skills.discover rescue nil
|
|
83
|
+
if @skills_registry && !@skills_registry.names.empty?
|
|
84
|
+
skill_text = @skills_registry.format_for_prompt
|
|
85
|
+
if !skill_text.empty? && @chat.messages.any? { |m| m.role == :system }
|
|
86
|
+
current = @chat.messages.find { |m| m.role == :system }.content.to_s
|
|
87
|
+
@chat.with_instructions(current + skill_text)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
@compactor&.chat = @chat
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def run(message, tools: nil)
|
|
94
|
+
raise "Session deleted" if @deleted
|
|
95
|
+
raise "Session already running" if @running
|
|
96
|
+
|
|
97
|
+
@running = true
|
|
98
|
+
@abort_requested = false
|
|
99
|
+
@turn_count = 0
|
|
100
|
+
@loop.reset!
|
|
101
|
+
|
|
102
|
+
emit(Events::SessionStart.new)
|
|
103
|
+
|
|
104
|
+
active_tools = resolve_tools(tools || [])
|
|
105
|
+
active_tools = @tools if active_tools.empty?
|
|
106
|
+
|
|
107
|
+
if active_tools.empty? && !@_no_tools_instructed
|
|
108
|
+
@chat.add_message(role: :system, content: "You have no tools available. Do not claim you can look up information or use tools of any kind. Just respond based on your existing knowledge.")
|
|
109
|
+
@_no_tools_instructed = true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
begin
|
|
113
|
+
@tool_executor.telemetry = @telemetry
|
|
114
|
+
|
|
115
|
+
response = @loop.run_turn(
|
|
116
|
+
chat: @chat,
|
|
117
|
+
message: message,
|
|
118
|
+
tools: active_tools,
|
|
119
|
+
tool_executor: @tool_executor,
|
|
120
|
+
compactor: @compactor,
|
|
121
|
+
hooks: @hooks,
|
|
122
|
+
event_emitter: self,
|
|
123
|
+
session_id: @id
|
|
124
|
+
)
|
|
125
|
+
rescue MaxTurnsExceeded => e
|
|
126
|
+
emit(Events::MaxTurnsExceeded.new(max_turns: @max_turns))
|
|
127
|
+
@telemetry.log(:max_turns_exceeded, session_id: @id, max_turns: @max_turns)
|
|
128
|
+
response = last_content
|
|
129
|
+
rescue LoopDetected => e
|
|
130
|
+
emit(Events::LoopDetected.new(tool_name: e.message, repeated_count: 3))
|
|
131
|
+
@telemetry.log(:loop_detected, session_id: @id, tool_name: e.message, repeated_count: 3)
|
|
132
|
+
response = last_content
|
|
133
|
+
rescue Ask::ContextLengthExceeded
|
|
134
|
+
if @compactor && !@compactor.overflow_recovered?
|
|
135
|
+
@compactor.recover_from_overflow
|
|
136
|
+
retry
|
|
137
|
+
end
|
|
138
|
+
response = "I'm sorry, the conversation has grown too long. Please start a new session."
|
|
139
|
+
rescue StandardError => e
|
|
140
|
+
emit(Events::Error.new(error: e.message, recoverable: true))
|
|
141
|
+
raise
|
|
142
|
+
ensure
|
|
143
|
+
@running = false
|
|
144
|
+
persist! if @persistence
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
@tool_calls_made = @tool_executor.total_executions
|
|
148
|
+
|
|
149
|
+
if @reflector && @reflector.reflect?(@tool_calls_made) && !@abort_requested
|
|
150
|
+
eval_result = @reflector.evaluate(response: response, event_emitter: self)
|
|
151
|
+
@telemetry.log(:reflection_end, session_id: @id, decision: eval_result[:decision], feedback: eval_result[:feedback])
|
|
152
|
+
|
|
153
|
+
if eval_result[:decision] == :improve && !@abort_requested
|
|
154
|
+
@chat.add_message(
|
|
155
|
+
role: :system,
|
|
156
|
+
content: "Improve your last response: #{eval_result[:feedback]}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
response = @loop.run_turn(
|
|
160
|
+
chat: @chat,
|
|
161
|
+
message: "",
|
|
162
|
+
tools: active_tools,
|
|
163
|
+
tool_executor: @tool_executor,
|
|
164
|
+
compactor: @compactor,
|
|
165
|
+
hooks: @hooks,
|
|
166
|
+
event_emitter: self,
|
|
167
|
+
session_id: @id
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if @meta_agent_config
|
|
173
|
+
@telemetry.increment_session_count!
|
|
174
|
+
try_auto_meta_agent
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
emit(Events::SessionEnd.new(result: response, turn_count: @turn_count, tool_calls_made: @tool_calls_made))
|
|
178
|
+
@messages = @chat.messages.dup
|
|
179
|
+
|
|
180
|
+
response
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def on_event(&block)
|
|
184
|
+
@event_handlers[:all] << block
|
|
185
|
+
self
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def on(type, &block)
|
|
189
|
+
@event_handlers[type] ||= []
|
|
190
|
+
@event_handlers[type] << block
|
|
191
|
+
self
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def emit(event)
|
|
195
|
+
@event_handlers[:all].each { |h| h.call(event) }
|
|
196
|
+
handlers = @event_handlers[event.class]
|
|
197
|
+
handlers&.each { |h| h.call(event) }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def running? = @running
|
|
201
|
+
def deleted? = @deleted
|
|
202
|
+
|
|
203
|
+
def save
|
|
204
|
+
persist! if @persistence
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def self.load(id, adapter:)
|
|
208
|
+
data = adapter.load(id)
|
|
209
|
+
return nil unless data
|
|
210
|
+
|
|
211
|
+
session = new(
|
|
212
|
+
id: data[:id],
|
|
213
|
+
model: data.dig(:metadata, :model),
|
|
214
|
+
tools: data.dig(:metadata, :tools)&.map(&:constantize) || [],
|
|
215
|
+
persistence: adapter
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
data[:messages].each do |msg|
|
|
219
|
+
session.chat.add_message(
|
|
220
|
+
role: msg[:role].to_sym,
|
|
221
|
+
content: msg[:content],
|
|
222
|
+
tool_call_id: msg[:tool_call_id]
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
session
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def delete
|
|
230
|
+
@deleted = true
|
|
231
|
+
@persistence&.delete(@id)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def abort
|
|
235
|
+
@abort_requested = true
|
|
236
|
+
end
|
|
237
|
+
|
data/lib/ask/agent/version.rb
CHANGED
data/lib/ask/agent.rb
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "ruby_llm"
|
|
4
3
|
require "fileutils"
|
|
5
4
|
require "json"
|
|
6
5
|
require "securerandom"
|
|
7
6
|
require "time"
|
|
7
|
+
require "ask/skills"
|
|
8
8
|
|
|
9
9
|
module Ask
|
|
10
10
|
module Agent
|
|
@@ -41,6 +41,7 @@ end
|
|
|
41
41
|
|
|
42
42
|
require_relative "agent/version"
|
|
43
43
|
require_relative "agent/events"
|
|
44
|
+
require_relative "agent/chat"
|
|
44
45
|
require_relative "agent/telemetry"
|
|
45
46
|
require_relative "agent/tool_abort_controller"
|
|
46
47
|
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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -9,6 +9,34 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ask-core
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ask-llm-providers
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.1'
|
|
12
40
|
- !ruby/object:Gem::Dependency
|
|
13
41
|
name: ask-tools
|
|
14
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -24,7 +52,7 @@ dependencies:
|
|
|
24
52
|
- !ruby/object:Gem::Version
|
|
25
53
|
version: '0.1'
|
|
26
54
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: ask-
|
|
55
|
+
name: ask-skills
|
|
28
56
|
requirement: !ruby/object:Gem::Requirement
|
|
29
57
|
requirements:
|
|
30
58
|
- - "~>"
|
|
@@ -38,19 +66,19 @@ dependencies:
|
|
|
38
66
|
- !ruby/object:Gem::Version
|
|
39
67
|
version: '0.1'
|
|
40
68
|
- !ruby/object:Gem::Dependency
|
|
41
|
-
name:
|
|
69
|
+
name: ask-tools-shell
|
|
42
70
|
requirement: !ruby/object:Gem::Requirement
|
|
43
71
|
requirements:
|
|
44
|
-
- - "
|
|
72
|
+
- - "~>"
|
|
45
73
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '1
|
|
47
|
-
type: :
|
|
74
|
+
version: '0.1'
|
|
75
|
+
type: :runtime
|
|
48
76
|
prerelease: false
|
|
49
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
78
|
requirements:
|
|
51
|
-
- - "
|
|
79
|
+
- - "~>"
|
|
52
80
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '1
|
|
81
|
+
version: '0.1'
|
|
54
82
|
- !ruby/object:Gem::Dependency
|
|
55
83
|
name: minitest
|
|
56
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -105,6 +133,7 @@ files:
|
|
|
105
133
|
- README.md
|
|
106
134
|
- lib/ask-agent.rb
|
|
107
135
|
- lib/ask/agent.rb
|
|
136
|
+
- lib/ask/agent/chat.rb
|
|
108
137
|
- lib/ask/agent/compactor.rb
|
|
109
138
|
- lib/ask/agent/configuration.rb
|
|
110
139
|
- lib/ask/agent/events.rb
|
|
@@ -118,6 +147,7 @@ files:
|
|
|
118
147
|
- lib/ask/agent/persistence/in_memory.rb
|
|
119
148
|
- lib/ask/agent/reflector.rb
|
|
120
149
|
- lib/ask/agent/session.rb
|
|
150
|
+
- lib/ask/agent/session_backup.rb
|
|
121
151
|
- lib/ask/agent/telemetry.rb
|
|
122
152
|
- lib/ask/agent/tool_abort_controller.rb
|
|
123
153
|
- lib/ask/agent/tool_executor.rb
|