Class: Ask::Attachment

Inherits:
Object
  • Object
show all
Defined in:
lib/ask/attachment.rb

Overview

A file a user attached to a message.

Sources (exactly one per attachment):

path:     local file path
url:      http(s) or data: URI
data:     raw bytes
io:       an IO/StringIO (read at render time)
blob:     a duck-typed object with +download+/+path+/+read+
        (e.g. an ActiveStorage blob)
file_id:  a provider-managed file reference (no bytes here)

Delivery modes:

:inline (default) — the bytes are sent to the model (via the
provider's serializers) so it can read the file. Provider
capability gates apply.
:context — only a manifest line reaches the model:
"[Attached file: name (mime, N bytes)]". The model knows the file
exists but never receives its content. Provider-agnostic — right
for agents that must not read uploaded files (e.g. a
requirements-gathering assistant).

Examples:

Inline image from a local path

Ask::Attachment.new(path: "receipt.png")

Context-only (know it exists, don't read it)

Ask::Attachment.new(path: "invoice.csv", delivery: :context)

ActiveStorage blob

Ask::Attachment.new(blob: work_request.source_files.first)

Constant Summary collapse

DELIVERY_MODES =
%i[inline context].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path: nil, url: nil, data: nil, io: nil, blob: nil, file_id: nil, filename: nil, mime_type: nil, delivery: :inline) ⇒ Attachment

Returns a new instance of Attachment.

Parameters:

  • path (String, nil) (defaults to: nil)

    local file path

  • url (String, nil) (defaults to: nil)

    http(s) or data: URI

  • data (String, nil) (defaults to: nil)

    raw bytes

  • io (IO, StringIO, nil) (defaults to: nil)
  • blob (Object, nil) (defaults to: nil)

    duck-typed blob (+download+/+path+/+read+)

  • file_id (String, nil) (defaults to: nil)

    provider-managed file reference

  • filename (String, nil) (defaults to: nil)

    override the derived filename

  • mime_type (String, nil) (defaults to: nil)

    override the derived MIME type

  • delivery (Symbol) (defaults to: :inline)

    :inline (default) or :context

Raises:

  • (ArgumentError)


65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/ask/attachment.rb', line 65

def initialize(path: nil, url: nil, data: nil, io: nil, blob: nil, file_id: nil,
               filename: nil, mime_type: nil, delivery: :inline)
  sources = { path: path, url: url, data: data, io: io, blob: blob, file_id: file_id }
  given = sources.count { |_, value| !value.nil? }
  raise ArgumentError, "Provide exactly one attachment source" unless given == 1

  @delivery = delivery.to_sym
  raise ArgumentError, "Unknown delivery mode: #{delivery.inspect}" unless DELIVERY_MODES.include?(@delivery)

  @path = path
  @io = io
  @blob = blob
  @url = url
  @file_id = file_id
  @raw_data = data
  @filename = filename || derive_filename
  @mime_type = mime_type || derive_mime_type
  @size = derive_size
  freeze
end

Instance Attribute Details

#deliverySymbol (readonly)

Returns :inline or :context.

Returns:

  • (Symbol)

    :inline or :context



48
49
50
# File 'lib/ask/attachment.rb', line 48

def delivery
  @delivery
end

#file_idString? (readonly)

Returns provider-managed file ID when given.

Returns:

  • (String, nil)

    provider-managed file ID when given



54
55
56
# File 'lib/ask/attachment.rb', line 54

def file_id
  @file_id
end

#filenameString? (readonly)

Returns original filename.

Returns:

  • (String, nil)

    original filename



39
40
41
# File 'lib/ask/attachment.rb', line 39

def filename
  @filename
end

#mime_typeString (readonly)

Returns MIME type.

Returns:

  • (String)

    MIME type



42
43
44
# File 'lib/ask/attachment.rb', line 42

def mime_type
  @mime_type
end

#sizeInteger? (readonly)

Returns size in bytes.

Returns:

  • (Integer, nil)

    size in bytes



45
46
47
# File 'lib/ask/attachment.rb', line 45

def size
  @size
end

#urlString? (readonly)

Returns URL (http(s) or data: URI) when given.

Returns:

  • (String, nil)

    URL (http(s) or data: URI) when given



51
52
53
# File 'lib/ask/attachment.rb', line 51

def url
  @url
end

Class Method Details

.wrap(value) ⇒ Attachment, Content::Block

Coerce a single value into an Ask::Attachment:

Attachment → itself
Content::Block → itself (already a block)
String → treated as a local file path
Hash → keyword constructor arguments
duck-typed blob (+download+/+path+/+read+) → blob source

Parameters:

  • value (Object)

Returns:



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/ask/attachment.rb', line 175

def self.wrap(value)
  case value
  when Attachment, Content::Block
    value
  when String
    new(path: value)
  when Hash
    new(**value.transform_keys(&:to_sym))
  else
    if value.respond_to?(:download) || value.respond_to?(:path) || value.respond_to?(:read)
      new(blob: value)
    else
      raise ArgumentError, "Cannot use #{value.class} as an attachment"
    end
  end
end

.wrap_all(values) ⇒ Array<Attachment, Content::Block>

Coerce a value or array of values into an array of attachments (and/or content blocks).

Parameters:

  • values (Object, Array<Object>)

Returns:



197
198
199
# File 'lib/ask/attachment.rb', line 197

def self.wrap_all(values)
  Array(values).map { |value| wrap(value) }
end

Instance Method Details

#base64String?

Base64-encoded bytes, when available.

Returns:

  • (String, nil)


113
114
115
116
# File 'lib/ask/attachment.rb', line 113

def base64
  bytes = data
  bytes && Base64.strict_encode64(bytes)
end

#context?Boolean

Returns whether only context (a manifest line) is sent.

Returns:

  • (Boolean)

    whether only context (a manifest line) is sent



157
158
159
# File 'lib/ask/attachment.rb', line 157

def context?
  @delivery == :context
end

#dataString?

Raw bytes, when the source can provide them (path/io/data/blob/data URIs). nil for URLs and provider file references — those are passed to the provider as-is.

Returns:

  • (String, nil)


91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/ask/attachment.rb', line 91

def data
  return @raw_data unless @raw_data.nil?
  return nil if @file_id
  return decode_data_uri if data_uri?

  if @path
    File.binread(@path)
  elsif @io
    @io.rewind if @io.respond_to?(:rewind)
    @io.read
  elsif @blob
    if @blob.respond_to?(:download)
      @blob.download
    elsif @blob.respond_to?(:read)
      @blob.read
    end
  end
end

#inline?Boolean

Returns whether the bytes are sent to the model.

Returns:

  • (Boolean)

    whether the bytes are sent to the model



162
163
164
# File 'lib/ask/attachment.rb', line 162

def inline?
  @delivery == :inline
end

#manifest_lineString

The manifest line rendered for :context attachments.

Returns:

  • (String)


128
129
130
131
# File 'lib/ask/attachment.rb', line 128

def manifest_line
  details = [mime_type, (size ? "#{size} bytes" : nil)].compact.join(", ")
  "[Attached file: #{filename || "file"} (#{details})]"
end

#to_contentContent::Block

Convert to the content block carried by the message.

:context attachments become a plain Content::Text manifest line; :inline attachments become the matching media/file block so the provider serializers can send the bytes.

Returns:



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/ask/attachment.rb', line 140

def to_content
  return Content::Text.new(manifest_line) if context?

  case type
  when :image
    Content::Image.new(url: url, base64: base64, mime_type: mime_type, file_id: file_id)
  when :audio
    Content::Audio.new(url: url, base64: base64, mime_type: mime_type, file_id: file_id)
  when :video
    Content::Video.new(url: url, base64: base64, mime_type: mime_type, file_id: file_id)
  else
    Content::File.new(data: data, mime_type: mime_type, filename: filename,
                      url: url, file_id: file_id)
  end
end

#typeSymbol

Broad category of the file (see Mime.classify).

Returns:

  • (Symbol)


121
122
123
# File 'lib/ask/attachment.rb', line 121

def type
  Mime.classify(mime_type)
end