ask-agent 0.38.0 → 0.39.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: f3486ed15f739d216a4b0dd46e8efdb29cc6cc7689a70ea4a2a6ef5477444037
4
- data.tar.gz: 628f14e1905695cf814eb3ff49012f924d30ce13c92c25ef425ec695dc37502f
3
+ metadata.gz: e2b75f968977ca6b41df200a5dbba36cc73d799bc74e91ad3a201a3b9a2c78e7
4
+ data.tar.gz: 19fe24495b1f70b44a320c98b77ee8c2fdf9d83a5642e4bf58c071f8e16ec45c
5
5
  SHA512:
6
- metadata.gz: 7cd3f07a7dbcaa19379815eb1999f225bef3baa938ab34e018c0d1c0d755edbd5e78ba67881f65098e4b3a59b2f6696e75e1a6bfa2bba3ba17c9cb151918c9ae
7
- data.tar.gz: 8644f8522aa5ea4719bb6e02d6dc19ce85d66f2e5e53765da02fcb8a002c67540f60ce31f787abb091a4e76b80a83fdb82d538b2866506385fa8c66e94f54d4a
6
+ metadata.gz: 5c9057c8b69cbe470beb46d9c656bf56ea15abf1d5328a93bc1a66cbef9c8f69f77aa0cfeb9f64cce264c60278f4df0c9290e3d89b71c42816ff2755d91b3e5e
7
+ data.tar.gz: 6df2b868b1430e00c9f2fab030ab48b1128de64b23a29b22f9c70963b977c777e20aaf75d5da529f772998116c453763b935ca92d4495efa21bd04efe309caf4
@@ -53,8 +53,9 @@ module Ask
53
53
  @prompt_caching = prompt_caching.nil? ? config.prompt_caching : prompt_caching
54
54
  end
55
55
 
56
- def ask(message = nil, &block)
57
- @messages << Ask::Message.new(role: :user, content: message.to_s) if message
56
+ def ask(message = nil, attachments: nil, &block)
57
+ validate_attachment_modalities!(attachments)
58
+ @messages << Ask::Message.new(role: :user, content: merge_attachments(message, attachments)) if message || attachments
58
59
 
59
60
  stream = block_given?
60
61
  tool_defs = @tools.map { |t| Ask::ToolDef.from_tool(t) }
@@ -79,10 +80,11 @@ module Ask
79
80
  response_msg
80
81
  end
81
82
 
82
- def add_message(role:, content: nil, tool_call_id: nil, tool_calls: nil)
83
+ def add_message(role:, content: nil, tool_call_id: nil, tool_calls: nil, attachments: nil)
84
+ validate_attachment_modalities!(attachments) if role == :user
83
85
  @messages << Ask::Message.new(
84
86
  role: role,
85
- content: content,
87
+ content: merge_attachments(content, attachments),
86
88
  tool_call_id: tool_call_id,
87
89
  tool_calls: tool_calls
88
90
  )
@@ -112,6 +114,42 @@ module Ask
112
114
 
113
115
  MAX_CHAT_RETRIES = 3
114
116
 
117
+ # Merge attachments into message content as content blocks: a plain
118
+ # string becomes a Text block followed by the attachment blocks;
119
+ # Array content has the blocks appended. Returns the content
120
+ # unchanged when there are no attachments.
121
+ def merge_attachments(content, attachments)
122
+ return content if attachments.nil? || attachments.empty?
123
+
124
+ blocks = content.is_a?(Array) ? content.dup : (content ? [Ask::Content::Text.new(content.to_s)] : [])
125
+ Ask::Attachment.wrap_all(attachments).each do |item|
126
+ blocks << (item.is_a?(Ask::Attachment) ? item.to_content : item)
127
+ end
128
+ blocks
129
+ end
130
+
131
+ # :inline attachments must be within the model's input modalities.
132
+ # :context attachments are plain text (a manifest line) and always
133
+ # supported. The check is skipped when the catalog has no modality
134
+ # info for the model.
135
+ def validate_attachment_modalities!(attachments)
136
+ return if attachments.nil? || attachments.empty?
137
+
138
+ inline = Ask::Attachment.wrap_all(attachments).select { |a| a.is_a?(Ask::Attachment) && a.inline? }
139
+ return if inline.empty?
140
+
141
+ modalities = @model_info.respond_to?(:modalities) ? @model_info.modalities : nil
142
+ supported = modalities.is_a?(Hash) ? Array(modalities[:input] || modalities["input"]) : []
143
+ return if supported.empty? || supported.include?("*")
144
+
145
+ unsupported = inline.reject { |a| supported.include?(a.type.to_s) }
146
+ return if unsupported.empty?
147
+
148
+ raise Ask::Agent::UnsupportedAttachmentError,
149
+ "Model #{@model_id} cannot receive #{unsupported.map(&:type).uniq.join(', ')} attachments " \
150
+ "(supported input modalities: #{supported.join(', ')})"
151
+ end
152
+
115
153
  def provider
116
154
  @test_provider || @provider ||= build_provider
117
155
  end
@@ -17,12 +17,12 @@ module Ask
17
17
  @max_consecutive_tool_turns = max_consecutive_tool_turns
18
18
  end
19
19
 
20
- def run_turn(chat:, message:, tools:, tool_executor:, compactor:, hooks:, event_emitter:, session_id: nil, persist: nil, tool_call_repair: nil, steer_source: nil)
20
+ def run_turn(chat:, message:, tools:, tool_executor:, compactor:, hooks:, event_emitter:, session_id: nil, persist: nil, tool_call_repair: nil, steer_source: nil, attachments: nil)
21
21
  raise MaxTurnsExceeded if @turn_count >= @max_turns
22
22
 
23
23
  event_emitter.emit(Events::TurnStart.new)
24
24
 
25
- response = chat.ask(message) do |chunk|
25
+ response = chat.ask(message, attachments: attachments) do |chunk|
26
26
  if chunk.content.to_s.strip.length > 0
27
27
  event_emitter.emit(Events::TextDelta.new(content: chunk.content))
28
28
  end
@@ -196,7 +196,7 @@ module Ask
196
196
  # (only when the +artifacts:+ option is enabled)
197
197
  attr_reader :artifact_store
198
198
 
199
- def run(message, tools: nil, reset: true)
199
+ def run(message, tools: nil, reset: true, attachments: nil)
200
200
  raise "Session deleted" if @deleted
201
201
  raise "Session already running" if @running
202
202
 
@@ -230,6 +230,7 @@ module Ask
230
230
  response = @loop.run_turn(
231
231
  chat: @chat,
232
232
  message: message,
233
+ attachments: attachments,
233
234
  tools: active_tools,
234
235
  tool_executor: @tool_executor,
235
236
  compactor: @compactor,
@@ -443,7 +444,7 @@ module Ask
443
444
  data[:messages].each do |msg|
444
445
  session.chat.add_message(
445
446
  role: msg[:role].to_sym,
446
- content: msg[:content],
447
+ content: deserialize_content(msg[:content]),
447
448
  tool_call_id: msg[:tool_call_id]
448
449
  )
449
450
  end
@@ -662,8 +663,11 @@ end
662
663
  # @param message [String]
663
664
  # @param expected_turn_id [Integer, nil] the turn id the caller
664
665
  # believes is current; nil skips the check
666
+ # @param attachments [Array<Ask::Attachment>, nil] files to attach
667
+ # (applied when the session is idle; queued steers keep the
668
+ # message text only — the loop resolves queued messages as text)
665
669
  # @return [Hash] {status: :stale|:queued|:steered, turn_id: Integer}
666
- def steer(message, expected_turn_id: nil)
670
+ def steer(message, expected_turn_id: nil, attachments: nil)
667
671
  @steer_mutex.synchronize do
668
672
  if expected_turn_id && expected_turn_id != @turn_id
669
673
  return { status: :stale, turn_id: @turn_id }
@@ -673,7 +677,7 @@ end
673
677
  return { status: :queued, turn_id: @turn_id }
674
678
  end
675
679
  end
676
- @chat.add_message(role: :user, content: message.to_s)
680
+ @chat.add_message(role: :user, content: message.to_s, attachments: attachments)
677
681
  { status: :steered, turn_id: @turn_id }
678
682
  end
679
683
 
@@ -959,7 +963,7 @@ end
959
963
  data[:messages].each do |msg|
960
964
  target.chat.add_message(
961
965
  role: msg[:role].to_sym,
962
- content: msg[:content],
966
+ content: self.class.deserialize_content(msg[:content]),
963
967
  tool_call_id: msg[:tool_call_id]
964
968
  )
965
969
  end
@@ -977,13 +981,26 @@ end
977
981
  @tools.reject { |t| t.is_a?(Ask::Skills::LoadSkillTool) }
978
982
  end
979
983
 
984
+ # Persist content blocks as their +to_h+ hashes so attachments
985
+ # survive save/load; plain messages stay strings.
986
+ def self.serialize_content(message)
987
+ message.content_blocks ? message.content_blocks.map(&:to_h) : message.content.to_s
988
+ end
989
+
990
+ # Rebuild message content from a persisted value: block hashes are
991
+ # reconstructed via Ask::Content.from_h (deep_symbolize_keys may have
992
+ # symbol keys — from_h normalizes).
993
+ def self.deserialize_content(content)
994
+ content.is_a?(Array) ? content.map { |block| Ask::Content.from_h(block) } : content
995
+ end
996
+
980
997
  def persist!
981
998
  payload = {
982
999
  id: @id,
983
1000
  messages: @chat.messages.map { |m|
984
1001
  {
985
1002
  role: m.role,
986
- content: m.content.to_s,
1003
+ content: self.class.serialize_content(m),
987
1004
  tool_call_id: m.tool_call_id,
988
1005
  created_at: Time.now.iso8601
989
1006
  }
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.38.0"
5
+ VERSION = "0.39.0"
6
6
  end
7
7
  end
data/lib/ask/agent.rb CHANGED
@@ -18,6 +18,7 @@ module Ask
18
18
  class ToolExecutionError < Error; end
19
19
  class CompactionFailed < Error; end
20
20
  class SessionNotPersisted < Error; end
21
+ class UnsupportedAttachmentError < Error; end
21
22
 
22
23
  class UnknownAgent < Error; end
23
24
 
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.38.0
4
+ version: 0.39.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto