Class: SpreeMenuChat::AnswerGenerator
- Inherits:
-
Object
- Object
- SpreeMenuChat::AnswerGenerator
- Defined in:
- app/services/spree_menu_chat/answer_generator.rb
Overview
Turns a customer's question into either an answer grounded in this store's real, currently-embedded menu/FAQ content, or a canned "I don't have that" fallback — never a guess from Gemini's general knowledge, and never a raw error bubbled up to the storefront widget.
The read-only guardrail (see the plan's Guardrails section) is enforced
structurally here, not just described in the system prompt: neither
#call nor #stream (M4's streaming counterpart, used by ChatController)
ever builds or passes a tools/function-declarations argument to
SpreeMenuChat::LlmClient#generate/#generate_stream, and no write-capable
Spree model (Spree::Order, Spree::LineItem, Spree::Cart, ...) is
referenced anywhere in this class or SpreeMenuChat::Retriever. Gemini
has no mechanism to invoke anything beyond generating text, so there is
nothing for a prompt-injection attempt (in the user's question, or
smuggled into embedded content) to hijack into an action — at worst it
can talk the model into an off-topic reply, which the system prompt
below asks it to refuse.
Class Method Summary collapse
- .call(question, store: Spree::Store.default) ⇒ Object
-
.stream(question, store: Spree::Store.default, &block) ⇒ Object
M4 — streaming counterpart of #call, used by ChatController.
Instance Method Summary collapse
- #call ⇒ Object
-
#initialize(question, store:) ⇒ AnswerGenerator
constructor
A new instance of AnswerGenerator.
- #stream(&block) ⇒ Object
Constructor Details
#initialize(question, store:) ⇒ AnswerGenerator
Returns a new instance of AnswerGenerator.
24 25 26 27 |
# File 'app/services/spree_menu_chat/answer_generator.rb', line 24 def initialize(question, store:) @question = question @store = store end |
Class Method Details
.call(question, store: Spree::Store.default) ⇒ Object
20 21 22 |
# File 'app/services/spree_menu_chat/answer_generator.rb', line 20 def self.call(question, store: Spree::Store.default) new(question, store: store).call end |
.stream(question, store: Spree::Store.default, &block) ⇒ Object
M4 — streaming counterpart of #call, used by ChatController. Yields
each real text fragment to the block as Gemini produces it. If
there's no relevant context, or generation fails, or Gemini never
actually yields any text (e.g. blocked by safety filtering — the
streaming analog of #call's extract_text(response) || fallback),
yields the same no-context fallback as a single fragment instead —
callers (ChatController's SSE writer) never need to special-case
"no answer" versus "one short answer," they just iterate.
48 49 50 |
# File 'app/services/spree_menu_chat/answer_generator.rb', line 48 def self.stream(question, store: Spree::Store.default, &block) new(question, store: store).stream(&block) end |
Instance Method Details
#call ⇒ Object
29 30 31 32 33 34 35 36 37 38 |
# File 'app/services/spree_menu_chat/answer_generator.rb', line 29 def call return budget_exceeded_fallback if SpreeMenuChat::TokenBudget.exceeded?(store: @store) @context = SpreeMenuChat::Retriever.call(@question, store: @store) return no_context_fallback if @context.empty? response = generate record_tokens(response&.dig('usageMetadata', 'totalTokenCount')) extract_text(response) || no_context_fallback end |
#stream(&block) ⇒ Object
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'app/services/spree_menu_chat/answer_generator.rb', line 52 def stream(&block) if SpreeMenuChat::TokenBudget.exceeded?(store: @store) block.call(budget_exceeded_fallback) return end @context = SpreeMenuChat::Retriever.call(@question, store: @store) if @context.empty? block.call(no_context_fallback) return end yielded_any = false begin total_tokens = SpreeMenuChat::LlmClient.for_store(@store).generate_stream(prompt, system_instruction: system_instruction) do |text| yielded_any = true block.call(text) end record_tokens(total_tokens) rescue SpreeMenuChat::RequestError, SpreeMenuChat::LlmClient::MissingCredentialsError => e SpreeMenuChat::Alerting.capture(e, context: { area: 'answer_generator_stream', store_id: @store.id }) end block.call(no_context_fallback) unless yielded_any end |