ask-agent 0.1.1 → 0.1.3
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 +28 -12
- data/lib/ask/agent/session.rb +34 -0
- data/lib/ask/agent/session_backup.rb +237 -0
- data/lib/ask/agent/version.rb +1 -1
- data/lib/ask/agent.rb +1 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bb3f240855fe04ef37b3d474cd4f9ddae5db28256673aa1d1804bdfbc1a8836c
|
|
4
|
+
data.tar.gz: 84684f83697510a261de607ede4a6aafd2a396a2151d169290769148214bc4a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41ac967bf49f11bea0d4a65d9984ddfc0838007096b153c2294691abd8ce3bdd60aae0890e10f7bfed39ce2ed0b7f71f577e33d10a029f1660c6b779b4117f13
|
|
7
|
+
data.tar.gz: e097d8c0bbd5bf5f78cccc49fd167d951119d91ee473915803c92dfbbae245167b95050ad321ba2ccfbed71fc3c224effe30252c2badeff8c6fbb07f624c801e
|
data/lib/ask/agent/chat.rb
CHANGED
|
@@ -26,8 +26,11 @@ module Ask
|
|
|
26
26
|
# internal usage (Hash of { id => ToolCallInfo }).
|
|
27
27
|
class Chat
|
|
28
28
|
# @return [String] model ID (e.g. "gpt-4o")
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
attr_reader :model_id
|
|
30
|
+
# @return [String] model ID (e.g. "gpt-4o")
|
|
31
|
+
def model
|
|
32
|
+
@model_id
|
|
33
|
+
end
|
|
31
34
|
# @return [Array<Ask::Message>] all messages in the conversation
|
|
32
35
|
attr_reader :messages
|
|
33
36
|
|
|
@@ -35,13 +38,17 @@ module Ask
|
|
|
35
38
|
# @param tools [Array<Ask::Tool>] tool instances available to the chat
|
|
36
39
|
# @param temperature [Float, nil] sampling temperature
|
|
37
40
|
# @param schema [Ask::Schema, Hash, nil] structured output schema
|
|
38
|
-
|
|
41
|
+
# @param assume_model_exists [Boolean] whether to skip model catalog lookup
|
|
42
|
+
# @param provider [String, Symbol, nil] provider slug to use (overrides model catalog)
|
|
43
|
+
def initialize(model:, tools: [], temperature: nil, schema: nil,
|
|
44
|
+
assume_model_exists: false, provider: nil, **)
|
|
39
45
|
@model_id = model.respond_to?(:id) ? model.id : model.to_s
|
|
40
|
-
@model_info = resolve_model(@model_id)
|
|
46
|
+
@model_info = resolve_model(@model_id) unless assume_model_exists
|
|
41
47
|
@tools = tools
|
|
42
48
|
@temperature = temperature
|
|
43
49
|
@schema = schema
|
|
44
50
|
@messages = []
|
|
51
|
+
@provider_override = provider
|
|
45
52
|
@provider = nil
|
|
46
53
|
end
|
|
47
54
|
|
|
@@ -123,6 +130,15 @@ module Ask
|
|
|
123
130
|
self
|
|
124
131
|
end
|
|
125
132
|
|
|
133
|
+
# Set the structured output schema and return self.
|
|
134
|
+
#
|
|
135
|
+
# @param schema [Ask::Schema, Hash] structured output schema
|
|
136
|
+
# @return [self]
|
|
137
|
+
def with_schema(schema)
|
|
138
|
+
@schema = schema.respond_to?(:to_json_schema) ? schema.to_json_schema : schema
|
|
139
|
+
self
|
|
140
|
+
end
|
|
141
|
+
|
|
126
142
|
# Clear all messages from the conversation.
|
|
127
143
|
def reset_messages!
|
|
128
144
|
@messages.clear
|
|
@@ -143,19 +159,19 @@ module Ask
|
|
|
143
159
|
end
|
|
144
160
|
|
|
145
161
|
def build_provider
|
|
146
|
-
slug = @model_info&.provider || "openai"
|
|
162
|
+
slug = @provider_override&.to_s || @model_info&.provider || "openai"
|
|
147
163
|
klass = Ask::Provider.resolve(slug)
|
|
148
164
|
klass.new(provider_config(slug))
|
|
149
165
|
end
|
|
150
166
|
|
|
151
167
|
def provider_config(slug, extra_keys: {})
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
env_key = "#{slug.upcase}_API_KEY"
|
|
169
|
+
key = ENV[env_key] || ENV["OPENCODE_API_KEY"] || ENV["OPENAI_API_KEY"]
|
|
170
|
+
base = ENV["#{slug.upcase}_API_BASE"] || ENV["OPENCODE_API_BASE"]
|
|
171
|
+
config = { api_key: key }
|
|
172
|
+
config[:"#{slug}_api_key"] = key
|
|
173
|
+
config[:"#{slug}_api_base"] = base if base
|
|
174
|
+
Ask::LLM::Config.new(config)
|
|
159
175
|
end
|
|
160
176
|
|
|
161
177
|
# Accumulate partial tool calls from streaming chunks.
|
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,
|
|
@@ -39,6 +41,16 @@ module Ask
|
|
|
39
41
|
@tool_executor = ToolExecutor.new(max_retries: max_tool_retries, parallel: parallel_tools)
|
|
40
42
|
@compactor = compactor ? build_compactor(compactor) : nil
|
|
41
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
|
|
42
54
|
@persistence = persistence
|
|
43
55
|
|
|
44
56
|
reflector_opts = reflector.is_a?(Hash) ? reflector : {}
|
|
@@ -202,6 +214,28 @@ module Ask
|
|
|
202
214
|
|
|
203
215
|
def abort_requested? = @abort_requested
|
|
204
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
|
+
|
|
205
239
|
def reset_messages!
|
|
206
240
|
@chat.reset_messages!
|
|
207
241
|
@messages = []
|
|
@@ -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
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.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: ask-skills
|
|
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: ask-tools-shell
|
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -133,6 +147,7 @@ files:
|
|
|
133
147
|
- lib/ask/agent/persistence/in_memory.rb
|
|
134
148
|
- lib/ask/agent/reflector.rb
|
|
135
149
|
- lib/ask/agent/session.rb
|
|
150
|
+
- lib/ask/agent/session_backup.rb
|
|
136
151
|
- lib/ask/agent/telemetry.rb
|
|
137
152
|
- lib/ask/agent/tool_abort_controller.rb
|
|
138
153
|
- lib/ask/agent/tool_executor.rb
|