Class: Mistri::Message

Inherits:
Object
  • Object
show all
Defined in:
lib/mistri/message.rb

Overview

One message in a conversation, the single shape every provider translates to and from its wire format. Immutable: streaming builds snapshots, sessions replay values, and nothing aliases across threads.

Roles: :system, :user, :assistant, :tool (a tool result linked back by tool_call_id). Assistant messages carry the model and provider that produced them, which is what lets a later turn replay history across models, plus usage and the stop reason.

ui is a tool result's host-only channel: it persists with the message and rides its :tool_result event, but no serializer ever sends it to a model. error is the machine-readable failure on an errored turn (ErrorData shape); error_message stays the human story.

Constant Summary collapse

ROLES =
%i[system user assistant tool].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(role:, content: nil, tool_call_id: nil, tool_name: nil, model: nil, provider: nil, usage: nil, stop_reason: nil, error_message: nil, ui: nil, error: nil) ⇒ Message

Returns a new instance of Message.

Raises:

  • (ArgumentError)


22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/mistri/message.rb', line 22

def initialize(role:, content: nil, tool_call_id: nil, tool_name: nil, model: nil,
               provider: nil, usage: nil, stop_reason: nil, error_message: nil, ui: nil,
               error: nil)
  role = role.to_sym
  raise ArgumentError, "unknown role #{role.inspect}" unless ROLES.include?(role)
  if stop_reason && !StopReason.valid?(stop_reason)
    raise ArgumentError, "unknown stop reason #{stop_reason.inspect}"
  end

  super(role:, content: Content.wrap(content).freeze, tool_call_id:, tool_name:,
        model:, provider:, usage:, stop_reason:, error_message:, ui:, error:)
end

Class Method Details

.assistant(content: nil, tool_calls: [], **meta) ⇒ Object



49
50
51
# File 'lib/mistri/message.rb', line 49

def self.assistant(content: nil, tool_calls: [], **meta)
  new(role: :assistant, content: [*Content.wrap(content), *tool_calls], **meta)
end

.from_h(hash) ⇒ Object



57
58
59
60
61
62
63
64
65
66
# File 'lib/mistri/message.rb', line 57

def self.from_h(hash)
  h = hash.transform_keys(&:to_s)
  new(role: h.fetch("role").to_sym,
      content: Array(h["content"]).map { |block| Content.from_h(block) },
      tool_call_id: h["tool_call_id"], tool_name: h["tool_name"],
      model: h["model"], provider: h["provider"]&.to_sym,
      usage: h["usage"] && Usage.from_h(h["usage"]),
      stop_reason: h["stop_reason"]&.to_sym, error_message: h["error_message"],
      ui: h["ui"], error: h["error"])
end

.system(content) ⇒ Object



35
# File 'lib/mistri/message.rb', line 35

def self.system(content) = new(role: :system, content:)

.tool(content:, tool_call_id:, tool_name: nil, ui: nil) ⇒ Object



53
54
55
# File 'lib/mistri/message.rb', line 53

def self.tool(content:, tool_call_id:, tool_name: nil, ui: nil)
  new(role: :tool, content:, tool_call_id:, tool_name:, ui:)
end

.user(content) ⇒ Object



37
# File 'lib/mistri/message.rb', line 37

def self.user(content) = new(role: :user, content:)

.user_with_images(content, images = []) ⇒ Object

A user turn carrying images alongside optional text.



40
41
42
43
44
45
46
47
# File 'lib/mistri/message.rb', line 40

def self.user_with_images(content, images = [])
  images = Array(images)
  return user(content) if images.empty?

  text = content.to_s
  blocks = text.empty? ? images : [Content::Text.new(text:), *images]
  new(role: :user, content: blocks)
end

Instance Method Details

#assistant?Boolean

Returns:

  • (Boolean)


70
# File 'lib/mistri/message.rb', line 70

def assistant? = role == :assistant

#system?Boolean

Returns:

  • (Boolean)


68
# File 'lib/mistri/message.rb', line 68

def system? = role == :system

#textObject

Every Text block joined, or nil when the turn carried no text.



74
75
76
77
# File 'lib/mistri/message.rb', line 74

def text
  texts = content.grep(Content::Text)
  texts.empty? ? nil : texts.map(&:text).join
end

#to_hObject

A serialization shape, not the member hash: rebuild with .from_h, never with new(**to_h).



85
86
87
88
# File 'lib/mistri/message.rb', line 85

def to_h
  { role:, content: content.map(&:to_h), tool_call_id:, tool_name:, model:,
    provider:, usage: usage&.to_h, stop_reason:, error_message:, ui:, error: }.compact
end

#tool?Boolean

Returns:

  • (Boolean)


71
# File 'lib/mistri/message.rb', line 71

def tool? = role == :tool

#tool_callsObject



79
# File 'lib/mistri/message.rb', line 79

def tool_calls = content.grep(ToolCall)

#tool_calls?Boolean

Returns:

  • (Boolean)


81
# File 'lib/mistri/message.rb', line 81

def tool_calls? = content.any?(ToolCall)

#user?Boolean

Returns:

  • (Boolean)


69
# File 'lib/mistri/message.rb', line 69

def user? = role == :user