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 five 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; RubyLLM.file_store builds them. The Rails integration keeps a persisted chatโs files in Active Storage.
chat.with_file_store(RubyLLM.file_store.directory("tmp/work"))
Write your own store by subclassing FileStore and implementing list, find, 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 find(path) = ...
def read(path) = ...
def write(path, content, content_type: nil) = ...
def delete(path) = ...
end
Constants
- CACHED_TEXTS
-
Converted texts each store keeps in memory, by default.
Public Class Methods
Source
# File lib/ruby_llm/file_store.rb, line 59 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 183 def attachment(path, file: nil) file ||= find(path) || raise(FileNotFoundError, "No file at #{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 195 def cache_text(file, text) @texts_lock.synchronize do @texts.delete(@texts.keys.first) if @texts.size >= CACHED_TEXTS @texts[text_key(file)] = text end end
Keeps text as the conversion of file.
Source
# File lib/ruby_llm/file_store.rb, line 190 def cached_text(file) @texts_lock.synchronize { @texts[text_key(file)] } end
Returns the converted text kept for file, or nil. Stores keep conversions in memory unless they override this and cache_text.
Source
# File lib/ruby_llm/file_store.rb, line 138 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.
Source
# File lib/ruby_llm/file_store.rb, line 153 def each(&) list.each(&) end
Yields each StoredFile, as list returns them.
Source
# File lib/ruby_llm/file_store.rb, line 158 def exists?(path) !find(path).nil? end
Returns whether there is a file at path.
Source
# File lib/ruby_llm/file_store.rb, line 117 def find(path) raise NotImplementedError, "#{self.class} must implement find" end
Returns the StoredFile at path, or nil.
Source
# File lib/ruby_llm/file_store.rb, line 112 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 148 def local_root nil end
Returns the folder on disk that holds the files, or nil. A store with one has code execution run in that folder, reading and writing the files in place; a store without one has each run copy in the files it names. FileStore::Directory returns its root. Override it in a store of your own that keeps its files in a folder, and give the store a folder of its own: the sandbox can change anything in it.
Source
# File lib/ruby_llm/file_store.rb, line 123 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 171 def text(path, config: nil) file = find(path) || raise(FileNotFoundError, "No file at #{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.
# File lib/ruby_llm/file_store.rb, line 132 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")