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
- .append(conversation_id, role:, content:, parent_id: nil, sidechain: false, message_group_id: nil, agent_id: nil, **metadata) ⇒ Object
- .branch(conversation_id, from_message_id:) ⇒ Object
- .build_chain(conversation_id, include_sidechains: false) ⇒ Object
- .cancel_skill!(conversation_id) ⇒ Object
- .clear_cancel_flag(conversation_id) ⇒ Object
- .clear_skill_state(conversation_id) ⇒ Object
- .conversation_exists?(conversation_id) ⇒ Boolean
- .create_conversation(conversation_id, **metadata) ⇒ Object
- .in_memory?(conversation_id) ⇒ Boolean
- .messages(conversation_id) ⇒ Object
- .migrate_parent_links!(conversation_id) ⇒ Object
- .raw_messages(conversation_id) ⇒ Object
- .read_metadata(conversation_id, tail_n: 20) ⇒ Object
- .read_sticky_state(conversation_id) ⇒ Object
- .replace(conversation_id, messages) ⇒ Object
- .reset! ⇒ Object
- .set_skill_state(conversation_id, skill_key:, resume_at:) ⇒ Object
- .sidechain_messages(conversation_id, agent_id: nil) ⇒ Object
- .skill_cancelled?(conversation_id) ⇒ Boolean
- .skill_state(conversation_id) ⇒ Object
- .store_metadata(conversation_id, title: nil, tags: nil, model: nil) ⇒ Object
- .write_sticky_state(conversation_id, state) ⇒ Object
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: , 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
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 = (conversation_id) target = raw.find { |m| m[:id] == } raise ArgumentError, "Message #{} 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 = (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
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
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 (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 (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 = (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, ) ensure_conversation(conversation_id) conversations[conversation_id][: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 (conversation_id, agent_id: nil) raw = (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
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: , 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 |