class RubyLLM::FileStore
A FileStore keeps a chatโs files: what people attach, what tools return, long tool results, and what code writes. Chat#with_file_store gives a chat one, and the model works with it through tools that list, read, search, write, and edit its files, so a file reaches the model only when the model asks for it.
chat = RubyLLM.chat.with_file_store chat.ask "Which clauses changed?", with: ["contract-v1.pdf", "contract-v2.pdf"] chat.file_store.list.map(&:path) # => ["contract-v1.pdf", "contract-v2.pdf"]
RubyLLM ships FileStore::Memory, the default, and FileStore::Directory, which keeps the files in a folder. The Rails integration keeps a persisted chatโs files in Active Storage.
chat.with_file_store("tmp/work")
Write your own store by subclassing FileStore and implementing list, [], read, write, and delete. RubyLLM only ever passes them paths FileStore.path accepts: relative, with forward slashes, and without empty, ., or .. parts.
class BucketStore < RubyLLM::FileStore
def initialize(bucket, prefix)
super()
@bucket = bucket
@prefix = prefix
end
def list(prefix = nil) = ...
def [](path) = ...
def read(path) = ...
def write(path, content, content_type: nil) = ...
def delete(path) = ...
end
The model must read a file before its tools change it, and a file that changed since it read it must be read again. The store remembers what the model read, and with_lock keeps two changes to the files from interleaving. Override with_lock in a store that several processes share.
Constants
- CACHED_TEXTS
-
Converted texts each store keeps in memory, by default.
Public Class Methods
Source
# File lib/ruby_llm/file_store.rb, line 70 def path(name) parts = name.to_s.tr('\\', '/').split('/').reject(&:empty?) parts.shift while parts.first == '.' if parts.empty? || parts.intersect?(%w[. ..]) || parts.any? { |part| part.match?(/[\x00-\x1f]/) } raise ArgumentError, "#{name.inspect} is not a path inside the chat's files" end parts.join('/') end
Returns name as a path inside a store: relative, with forward slashes, and with a leading ./ or / and repeated slashes removed.
RubyLLM::FileStore.path("./reports//q3.pdf") # => "reports/q3.pdf"
Raises ArgumentError for an empty path or one with . or .. parts.
Public Instance Methods
Source
# File lib/ruby_llm/file_store.rb, line 158 def [](path) raise NotImplementedError, "#{self.class} must implement []" end
Returns the StoredFile at path, or nil when there is none.
chat.file_store["notes/plan.md"] # => #<RubyLLM::StoredFile path: "notes/plan.md", ...>
Source
# File lib/ruby_llm/file_store.rb, line 266 def attachment(path, file: nil) file ||= find(path) Attachment.new(StringIO.new(read(file.path)), filename: file.filename) end
Returns the file at path as an Attachment, to send it to a model. Raises FileNotFoundError when there is none.
Source
# File lib/ruby_llm/file_store.rb, line 189 def delete(path) raise NotImplementedError, "#{self.class} must implement delete" end
Deletes the file at path and returns its StoredFile, or nil when there was none. Raises ArgumentError when path is a folder.
chat.file_store.delete("notes/plan.md")
Source
# File lib/ruby_llm/file_store.rb, line 198 def delete_folder(path) path = FileStore.path(path) refuse_file(path) if self[path] list(path).filter_map { |file| delete(file.path) } end
Deletes everything inside the folder path and returns the deleted StoredFile objects, sorted by path. Raises ArgumentError when path is a file.
chat.file_store.delete_folder("reports") # => [#<RubyLLM::StoredFile path: "reports/q3.pdf", ...>]
Source
# File lib/ruby_llm/file_store.rb, line 215 def each(&) list.each(&) end
Yields each StoredFile, as list returns them.
Source
# File lib/ruby_llm/file_store.rb, line 220 def exist?(path) !self[path].nil? end
Returns whether there is a file at path.
Source
# File lib/ruby_llm/file_store.rb, line 166 def find(path) self[path] || raise(FileNotFoundError, "No file at #{FileStore.path(path)}") end
Returns the StoredFile at path. Raises FileNotFoundError when there is none.
chat.file_store.find("notes/plan.md")
Source
# File lib/ruby_llm/file_store.rb, line 151 def list(prefix = nil) raise NotImplementedError, "#{self.class} must implement list" end
Returns the files inside the folder prefix, or every file, as StoredFile objects sorted by path.
chat.file_store.list("reports")
Source
# File lib/ruby_llm/file_store.rb, line 172 def read(path) raise NotImplementedError, "#{self.class} must implement read" end
Returns the content of the file at path as a binary String. Raises FileNotFoundError when there is none.
Source
# File lib/ruby_llm/file_store.rb, line 254 def text(path, config: nil) file = find(path) cached = cached_text(file) return cached if cached Converters.convert(attachment(file.path, file:), config: config || RubyLLM.config).tap do |text| cache_text(file, text) if text end end
Returns the text of the file at path: its content when it is text, or what a converter makes of it, such as the text OCR reads from a PDF. Returns nil when no converter can read the file. Conversions are kept, so each version of a file is converted once. Raises FileNotFoundError when there is no file at path.
chat.file_store.text("contract.pdf") # => "## Page 1\n\nThis agreement..."
See Configuration#file_converters for adding converters.
Source
# File lib/ruby_llm/file_store.rb, line 241 def with_lock(&) @store_lock.synchronize(&) end
Runs the block with the store locked and returns its value, so a tool that reads a file and writes it back sees no other change in between. It is reentrant. Stores lock within this process unless they override it.
# File lib/ruby_llm/file_store.rb, line 181 def write(path, content, content_type: nil) raise NotImplementedError, "#{self.class} must implement write" end
Writes content, a String or an IO, to path, replacing any file there, and returns its StoredFile. The content type is detected from the path and the content unless given.
chat.file_store.write("notes/plan.md", "# Plan")