Class: Raif::Conversation
- Inherits:
-
ApplicationRecord
- Object
- ApplicationRecord
- Raif::Conversation
- Includes:
- Raif::Concerns::HasAvailableModelTools, Raif::Concerns::HasLlm, Raif::Concerns::HasPromptTemplates, Raif::Concerns::HasRequestedLanguage, Raif::Concerns::LlmPromptCaching, Raif::Concerns::LlmResponseParsing
- Defined in:
- app/models/raif/conversation.rb
Overview
Schema Information
Table name: raif_conversations
id :bigint not null, primary key available_model_tools :jsonb not null available_user_tools :jsonb not null conversation_entries_count :integer default(0), not null creator_type :string not null generating_entry_response :boolean default(FALSE), not null latest_entry_at :datetime llm_messages_max_length :integer llm_model_key :string not null requested_language_key :string response_format :integer default("text"), not null source_type :string system_prompt :text type :string not null created_at :datetime not null updated_at :datetime not null creator_id :bigint not null source_id :bigint
Indexes
index_raif_conversations_on_created_at (created_at) index_raif_conversations_on_creator (creator_type,creator_id) index_raif_conversations_on_source (source_type,source_id)
Constant Summary
Constants included from Raif::Concerns::LlmResponseParsing
Raif::Concerns::LlmResponseParsing::ASCII_CONTROL_CHARS
Class Method Summary collapse
- .before_prompt_model_for_entry_response(&block) ⇒ Object
- .before_prompt_model_for_entry_response_blocks ⇒ Object
Instance Method Summary collapse
- #available_user_tool_classes ⇒ Object
- #build_system_prompt ⇒ Object
-
#initial_chat_message ⇒ Object
i18n-tasks-use t('raif.conversation.initial_chat_message').
- #initial_chat_message_partial_path ⇒ Object
- #llm_messages ⇒ Object
-
#on_entry_finalized(entry:) ⇒ Object
Called exactly once per ConversationEntry immediately after the entry has been successfully finalized (model response saved, all developer- managed tool calls validated and invoked, entry transitioned to
completed!). - #process_model_response_message(message:, entry:) ⇒ Object
- #prompt_model_for_entry_response(entry:, extra_messages: [], &block) ⇒ Object
- #system_prompt_intro ⇒ Object
Methods included from Raif::Concerns::LlmResponseParsing
#parse_html_response, #parse_json_response, #parsed_response
Methods included from Raif::Concerns::HasAvailableModelTools
Methods included from Raif::Concerns::HasRequestedLanguage
#requested_language_name, #system_prompt_language_preference
Methods included from Raif::Concerns::HasLlm
Methods included from Raif::Concerns::HasPromptTemplates
Class Method Details
.before_prompt_model_for_entry_response(&block) ⇒ Object
45 46 47 48 |
# File 'app/models/raif/conversation.rb', line 45 def before_prompt_model_for_entry_response(&block) @before_prompt_model_for_entry_response_blocks ||= [] @before_prompt_model_for_entry_response_blocks << block if block end |
.before_prompt_model_for_entry_response_blocks ⇒ Object
50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'app/models/raif/conversation.rb', line 50 def before_prompt_model_for_entry_response_blocks blocks = [] # Collect blocks from ancestors (in reverse order so parent blocks run first) ancestors.reverse_each do |klass| if klass.instance_variable_defined?(:@before_prompt_model_for_entry_response_blocks) blocks.concat(klass.instance_variable_get(:@before_prompt_model_for_entry_response_blocks)) end end blocks end |
Instance Method Details
#available_user_tool_classes ⇒ Object
216 217 218 |
# File 'app/models/raif/conversation.rb', line 216 def available_user_tool_classes available_user_tools.map(&:constantize) end |
#build_system_prompt ⇒ Object
74 75 76 77 78 79 |
# File 'app/models/raif/conversation.rb', line 74 def build_system_prompt <<~PROMPT.strip #{system_prompt_intro} #{system_prompt_language_preference} PROMPT end |
#initial_chat_message ⇒ Object
i18n-tasks-use t('raif.conversation.initial_chat_message')
87 88 89 |
# File 'app/models/raif/conversation.rb', line 87 def I18n.t("#{self.class.name.underscore.gsub("/", ".")}.initial_chat_message") end |
#initial_chat_message_partial_path ⇒ Object
91 92 93 |
# File 'app/models/raif/conversation.rb', line 91 def "raif/conversations/initial_chat_message" end |
#llm_messages ⇒ Object
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'app/models/raif/conversation.rb', line 177 def = [] # Apply max length limit to entries if configured (nil means no limit) included_entries = entries.oldest_first.includes(:raif_model_tool_invocations) included_entries = included_entries.last() if .present? included_entries.each do |entry| unless entry..blank? << Raif::Messages::UserMessage.new(content: entry.).to_h end next unless entry.completed? tool_invocations = entry.raif_model_tool_invocations.to_a if tool_invocations.any? # First tool call includes the assistant's message (if any). # For the result payload we send the model-facing observation when the tool # opts into observations, while keeping the raw invocation.result persisted # for admin/UI rendering. first_invocation = tool_invocations.shift << first_invocation.(assistant_message: entry..presence) << first_invocation.(result: tool_result_for_llm(first_invocation)) # Remaining tool calls (if multiple) tool_invocations.each do |tool_invocation| << tool_invocation. << tool_invocation.(result: tool_result_for_llm(tool_invocation)) end elsif entry..present? # No tool calls, just a regular assistant response << Raif::Messages::AssistantMessage.new(content: entry.).to_h end end end |
#on_entry_finalized(entry:) ⇒ Object
Called exactly once per Raif::ConversationEntry immediately after the
entry has been successfully finalized (model response saved, all developer-
managed tool calls validated and invoked, entry transitioned to
completed!). This is the correct place for per-entry side effects such
as creating dependent records, enqueuing follow-up work, or broadcasting
UI updates tied to the final response — anything you do NOT want
re-executing for attempts that were discarded by the retry loop, or for
intermediate streaming callbacks.
No-op by default. Subclasses may override.
173 174 175 |
# File 'app/models/raif/conversation.rb', line 173 def on_entry_finalized(entry:) # no-op by default. end |
#process_model_response_message(message:, entry:) ⇒ Object
149 150 151 152 153 154 155 156 157 158 159 |
# File 'app/models/raif/conversation.rb', line 149 def (message:, entry:) # no-op by default. # Override in subclasses for type-specific processing of the model response message. # # IMPORTANT: this method is invoked on every streaming chunk and on every # retry attempt made by {Raif::ConversationEntry#process_entry!}. Do NOT # put persistent side effects (DB writes, broadcasts, external calls) in # here — those belong in {#on_entry_finalized}, which runs exactly once # per entry after validation and tool invocation have succeeded. end |
#prompt_model_for_entry_response(entry:, extra_messages: [], &block) ⇒ Object
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'app/models/raif/conversation.rb', line 102 def prompt_model_for_entry_response(entry:, extra_messages: [], &block) self.class.before_prompt_model_for_entry_response_blocks.each do |callback_block| instance_exec(entry, &callback_block) end self.system_prompt = build_system_prompt self. = true save! = .concat() if .present? model_completion = llm.chat( messages: , source: entry, response_format: response_format.to_sym, system_prompt: system_prompt, available_model_tools: available_model_tools, anthropic_prompt_caching_enabled: self.class.anthropic_prompt_caching_enabled, bedrock_prompt_caching_enabled: self.class.bedrock_prompt_caching_enabled, &block ) self. = false save! model_completion rescue StandardError => e self. = false save! Rails.logger.error("Error processing conversation entry ##{entry.id}. #{e.}") Rails.logger.error(e.backtrace.join("\n")) entry.failed! if defined?(Airbrake) notice = Airbrake.build_notice(e) notice[:context][:component] = "raif_conversation" notice[:context][:action] = "prompt_model_for_entry_response" Airbrake.notify(notice) end nil end |