Module: Legion::LLM::Inference::Conversation

Extended by:
Legion::Logging::Helper
Defined in:
lib/legion/llm/inference/conversation.rb

Constant Summary collapse

MAX_CONVERSATIONS =
256
METADATA_ROLE =
:__metadata__
CURATED_ROLE =
:__curated__

Class Method Summary collapse

Class Method Details

.append(conversation_id, role:, content:, parent_id: nil, sidechain: false, message_group_id: nil, agent_id: nil, **metadata) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/legion/llm/inference/conversation.rb', line 17

def append(conversation_id, role:, content:, parent_id: nil, sidechain: false,
           message_group_id: nil, agent_id: nil, **)
  ensure_conversation(conversation_id)
  id  = SecureRandom.uuid
  seq = next_seq(conversation_id)
  msg = {
    id:               id,
    seq:              seq,
    role:             role,
    content:          content,
    parent_id:        parent_id,
    sidechain:        sidechain,
    message_group_id: message_group_id,
    agent_id:         agent_id,
    created_at:       Time.now,
    **
  }
  conversations[conversation_id][:messages] << msg
  touch(conversation_id)
  msg
end

.branch(conversation_id, from_message_id:) ⇒ Object

Raises:

  • (ArgumentError)


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/legion/llm/inference/conversation.rb', line 68

def branch(conversation_id, from_message_id:)
  raw = all_raw_messages(conversation_id)
  target = raw.find { |m| m[:id] == from_message_id }
  raise ArgumentError, "Message #{from_message_id} not found in #{conversation_id}" unless target

  chain = reconstruct_chain(raw)
  cutoff_seq = target[:seq]
  prefix = chain.select { |m| m[:seq] <= cutoff_seq }

  new_id = SecureRandom.uuid
  create_conversation(new_id)
  prefix.each_with_index do |msg, i|
    new_msg = msg.merge(seq: i + 1, id: SecureRandom.uuid, parent_id: nil, created_at: Time.now)
    conversations[new_id][:messages] << new_msg
  end
  touch(new_id)
  new_id
end

.build_chain(conversation_id, include_sidechains: false) ⇒ Object



54
55
56
57
58
59
# File 'lib/legion/llm/inference/conversation.rb', line 54

def build_chain(conversation_id, include_sidechains: false)
  raw = all_raw_messages(conversation_id)
  raw = raw.reject { |m| m[:sidechain] } unless include_sidechains
  raw = raw.reject { |m| internal_role?(m[:role]) }
  reconstruct_chain(raw)
end

.cancel_skill!(conversation_id) ⇒ Object



177
178
179
180
181
182
183
184
185
# File 'lib/legion/llm/inference/conversation.rb', line 177

def cancel_skill!(conversation_id)
  ensure_conversation(conversation_id)
  state = conversations[conversation_id].delete(:skill_state)
  if state
    conversations[conversation_id][:skill_cancelled] = true
    touch(conversation_id)
  end
  state
end

.clear_cancel_flag(conversation_id) ⇒ Object



193
194
195
196
197
198
# File 'lib/legion/llm/inference/conversation.rb', line 193

def clear_cancel_flag(conversation_id)
  return unless in_memory?(conversation_id)

  conversations[conversation_id].delete(:skill_cancelled)
  touch(conversation_id)
end

.clear_skill_state(conversation_id) ⇒ Object



170
171
172
173
174
175
# File 'lib/legion/llm/inference/conversation.rb', line 170

def clear_skill_state(conversation_id)
  return unless in_memory?(conversation_id)

  conversations[conversation_id].delete(:skill_state)
  touch(conversation_id)
end

.conversation_exists?(conversation_id) ⇒ Boolean

Returns:

  • (Boolean)


144
145
146
# File 'lib/legion/llm/inference/conversation.rb', line 144

def conversation_exists?(conversation_id)
  in_memory?(conversation_id)
end

.create_conversation(conversation_id, **metadata) ⇒ Object



131
132
133
134
# File 'lib/legion/llm/inference/conversation.rb', line 131

def create_conversation(conversation_id, **)
  conversations[conversation_id] = { messages: [], metadata: , lru_tick: next_tick }
  evict_if_needed
end

.in_memory?(conversation_id) ⇒ Boolean

Returns:

  • (Boolean)


148
149
150
# File 'lib/legion/llm/inference/conversation.rb', line 148

def in_memory?(conversation_id)
  conversations.key?(conversation_id)
end

.messages(conversation_id) ⇒ Object



39
40
41
42
43
44
45
# File 'lib/legion/llm/inference/conversation.rb', line 39

def messages(conversation_id)
  return [] unless in_memory?(conversation_id)

  touch(conversation_id)
  raw = conversations[conversation_id][:messages].reject { |m| internal_role?(m[:role]) }
  chain_or_seq(raw)
end

.migrate_parent_links!(conversation_id) ⇒ Object



200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/legion/llm/inference/conversation.rb', line 200

def migrate_parent_links!(conversation_id)
  ensure_conversation(conversation_id)
  msgs = conversations[conversation_id][:messages].sort_by { |m| m[:seq] }
  return if msgs.empty?
  return if msgs.any? { |m| m[:parent_id] }

  prev_id = nil
  msgs.each do |msg|
    msg[:parent_id] = prev_id
    prev_id = msg[:id] ||= SecureRandom.uuid
  end

  touch(conversation_id)
end

.raw_messages(conversation_id) ⇒ Object



47
48
49
50
51
52
# File 'lib/legion/llm/inference/conversation.rb', line 47

def raw_messages(conversation_id)
  return [] unless in_memory?(conversation_id)

  touch(conversation_id)
  conversations[conversation_id][:messages].dup
end

.read_metadata(conversation_id, tail_n: 20) ⇒ Object



106
107
108
109
110
111
112
113
114
115
116
# File 'lib/legion/llm/inference/conversation.rb', line 106

def (conversation_id, tail_n: 20)
  raw = all_raw_messages(conversation_id)
  tail = raw.last(tail_n).select { |m| m[:role] == METADATA_ROLE }
  return nil if tail.empty?

  entry = tail.last
  Legion::JSON.parse(entry[:content])
rescue Legion::JSON::ParseError => e
  handle_exception(e, level: :debug, handled: true, operation: 'llm.conversation.metadata_json_parse')
  nil
end

.read_sticky_state(conversation_id) ⇒ Object



118
119
120
121
122
# File 'lib/legion/llm/inference/conversation.rb', line 118

def read_sticky_state(conversation_id)
  return {}.freeze unless in_memory?(conversation_id)

  conversations[conversation_id][:sticky_state] ||= {}
end

.replace(conversation_id, messages) ⇒ Object



136
137
138
139
140
141
142
# File 'lib/legion/llm/inference/conversation.rb', line 136

def replace(conversation_id, messages)
  ensure_conversation(conversation_id)
  conversations[conversation_id][:messages] = messages.each_with_index.map do |msg, i|
    msg.merge(seq: i + 1, created_at: msg[:created_at] || Time.now)
  end
  touch(conversation_id)
end

.reset!Object



152
153
154
155
# File 'lib/legion/llm/inference/conversation.rb', line 152

def reset!
  @conversations = {}
  @lru_counter   = 0
end

.set_skill_state(conversation_id, skill_key:, resume_at:) ⇒ Object



157
158
159
160
161
# File 'lib/legion/llm/inference/conversation.rb', line 157

def set_skill_state(conversation_id, skill_key:, resume_at:)
  ensure_conversation(conversation_id)
  conversations[conversation_id][:skill_state] = { skill_key: skill_key, resume_at: resume_at }
  touch(conversation_id)
end

.sidechain_messages(conversation_id, agent_id: nil) ⇒ Object



61
62
63
64
65
66
# File 'lib/legion/llm/inference/conversation.rb', line 61

def sidechain_messages(conversation_id, agent_id: nil)
  raw = all_raw_messages(conversation_id)
  result = raw.select { |m| m[:sidechain] && !internal_role?(m[:role]) }
  result = result.select { |m| m[:agent_id] == agent_id } unless agent_id.nil?
  result.sort_by { |m| m[:seq] }
end

.skill_cancelled?(conversation_id) ⇒ Boolean

Returns:

  • (Boolean)


187
188
189
190
191
# File 'lib/legion/llm/inference/conversation.rb', line 187

def skill_cancelled?(conversation_id)
  return false unless in_memory?(conversation_id)

  conversations[conversation_id][:skill_cancelled] == true
end

.skill_state(conversation_id) ⇒ Object



163
164
165
166
167
168
# File 'lib/legion/llm/inference/conversation.rb', line 163

def skill_state(conversation_id)
  return nil unless in_memory?(conversation_id)

  touch(conversation_id)
  conversations[conversation_id][:skill_state]&.dup
end

.store_metadata(conversation_id, title: nil, tags: nil, model: nil) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/legion/llm/inference/conversation.rb', line 87

def (conversation_id, title: nil, tags: nil, model: nil)
  ensure_conversation(conversation_id)
  payload = { title: title, tags: tags, model: model }.compact
  msg = {
    id:               SecureRandom.uuid,
    seq:              next_seq(conversation_id),
    role:             METADATA_ROLE,
    content:          payload.to_json,
    parent_id:        nil,
    sidechain:        false,
    message_group_id: nil,
    agent_id:         nil,
    created_at:       Time.now
  }
  conversations[conversation_id][:messages] << msg
  touch(conversation_id)
  msg
end

.write_sticky_state(conversation_id, state) ⇒ Object



124
125
126
127
128
129
# File 'lib/legion/llm/inference/conversation.rb', line 124

def write_sticky_state(conversation_id, state)
  return unless in_memory?(conversation_id)

  conversations[conversation_id][:sticky_state] = state
  touch(conversation_id)
end