Class: Collavre::AiClient
- Inherits:
-
Object
- Object
- Collavre::AiClient
- Defined in:
- app/services/collavre/ai_client.rb
Constant Summary collapse
- SYSTEM_INSTRUCTIONS =
<<~PROMPT.freeze You are a senior expert teammate. Respond: - Be concise and focus on the essentials (avoid unnecessary verbosity). - Use short bullet points only when helpful. - State only what you're confident about; briefly note any uncertainty. - Respond in the asker's language (prefer the latest user message). Keep code and error messages in their original form. PROMPT
Instance Attribute Summary collapse
-
#last_input_tokens ⇒ Object
readonly
Returns the value of attribute last_input_tokens.
-
#last_output_tokens ⇒ Object
readonly
Returns the value of attribute last_output_tokens.
Instance Method Summary collapse
-
#ask(prompt) ⇒ Object
Ask a follow-up question using the existing conversation context.
- #chat(contents, tools: [], &block) ⇒ Object
-
#initialize(vendor:, model:, system_prompt:, llm_api_key: nil, gateway_url: nil, context: {}, log_interactions: true) ⇒ AiClient
constructor
log_interactions: persist each call to ActivityLog.
Constructor Details
#initialize(vendor:, model:, system_prompt:, llm_api_key: nil, gateway_url: nil, context: {}, log_interactions: true) ⇒ AiClient
log_interactions: persist each call to ActivityLog. Default true. Pass false for ephemeral, high-frequency calls on text the user has not submitted (e.g. inline typo correction on debounced typing) so private drafts are never written to server-side activity logs.
17 18 19 20 21 22 23 24 25 26 27 |
# File 'app/services/collavre/ai_client.rb', line 17 def initialize(vendor:, model:, system_prompt:, llm_api_key: nil, gateway_url: nil, context: {}, log_interactions: true) @vendor = vendor @model = model @system_prompt = system_prompt @llm_api_key = llm_api_key @gateway_url = gateway_url @context = context @log_interactions = log_interactions @last_input_tokens = 0 @last_output_tokens = 0 end |
Instance Attribute Details
#last_input_tokens ⇒ Object (readonly)
Returns the value of attribute last_input_tokens.
11 12 13 |
# File 'app/services/collavre/ai_client.rb', line 11 def last_input_tokens @last_input_tokens end |
#last_output_tokens ⇒ Object (readonly)
Returns the value of attribute last_output_tokens.
11 12 13 |
# File 'app/services/collavre/ai_client.rb', line 11 def last_output_tokens @last_output_tokens end |
Instance Method Details
#ask(prompt) ⇒ Object
Ask a follow-up question using the existing conversation context. Used to generate approval summaries with full conversation history. Returns the response content string, or nil on failure.
101 102 103 104 105 106 107 108 109 110 111 |
# File 'app/services/collavre/ai_client.rb', line 101 def ask(prompt) return nil unless @conversation # Disable tool calls for summary generation to avoid recursive approval @conversation.with_tools(replace: true) response = @conversation.ask(prompt) response&.content&.strip.presence rescue StandardError => e Rails.logger.warn("AiClient#ask failed: #{e.class} #{e.}") nil end |
#chat(contents, tools: [], &block) ⇒ Object
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'app/services/collavre/ai_client.rb', line 29 def chat(contents, tools: [], &block) response_content = +"" = nil input_tokens = nil output_tokens = nil normalized_vendor = vendor.to_s.downcase unless VENDOR_TO_PROVIDER.key?(normalized_vendor) Rails.logger.warn "Unsupported LLM vendor '#{@vendor}'. Attempting to use default (google)." end @conversation = build_conversation(tools) (@conversation, contents) response = @conversation.complete do |chunk| delta = extract_chunk_content(chunk) next if delta.blank? response_content << delta yield delta if block_given? end if response response_content = response.content.to_s if response.content.present? # Extract token usage directly from response object (RubyLLM style) if response.respond_to?(:input_tokens) input_tokens = response.input_tokens end if response.respond_to?(:output_tokens) output_tokens = response.output_tokens end end response_content.presence rescue ApprovalPendingError # Preserve conversation for follow-up (e.g. generating approval summary) raise rescue CancelledError raise # Re-raise cancellation errors without catching them rescue StandardError => e = "[#{e.class.name}] #{e.}" # When log_interactions is false (inline typo correction runs on the user's # *unsubmitted* draft), the LLM error message can echo the request text. Log # only the error class to app logs so private drafts never leak — matching the # no-log guarantee already enforced on the parse path (TypoCorrector) and the # ActivityLog gate below. error_message stays intact for the gated ensure log # and the streamed yield (which goes back to the same user). Rails.logger.error "AI Client error: #{@log_interactions ? : "[#{e.class.name}]"}" Rails.logger.error "Partial response length: #{response_content.length} chars" if response_content.present? Rails.logger.debug e.backtrace.join("\n") yield "\n\n⚠️ AI Error: #{}" if block_given? nil ensure @last_input_tokens = input_tokens || 0 @last_output_tokens = output_tokens || 0 if @log_interactions log_interaction( messages: @conversation&.&.to_a || Array(contents), tools: @conversation&.tools&.to_a || [], response_content: response_content.presence, error_message: , input_tokens: input_tokens, output_tokens: output_tokens ) end end |