Class: Assistant::ProductToolBuilder

Inherits:
Object
  • Object
show all
Defined in:
app/services/assistant/product_tool_builder.rb

Overview

Builds RubyLLM::Tool subclasses for product catalog queries.
Provides 3 tools:
get_product — full rendered snapshot of a single SKU
search_products — semantic search with pricing + availability
browse_product_line — list products within a product line hierarchy

All tools are locale-aware (en-US = catalog 1, en-CA = catalog 2).
Pricing and availability flags mirror what ProductCatalogPresenter renders.

Usage (via ChatToolBuilder):
tools = Assistant::ProductToolBuilder.tools

Constant Summary collapse

CRM_ITEM_URL =

URL for crm item.

"#{CRM_URL}/en-US/items".freeze
CRM_PRODUCT_LINE_URL =

URL for crm product line (descriptions editor).

"#{CRM_URL}/product_lines".freeze
WEB_PRODUCT_URL =

URL for web product.

"https://www.warmlyyours.com"
PRODUCT_DATA_GUIDE =

Data guide injected into tool descriptions so the LLM understands
the rendering flags and pricing cascade without a separate prompt.

<<~GUIDE
  ## Product Data Guide

  ### Page title cascade (what renders as the <title> tag)
    seo_title  →  public_short_name  →  item_name
    effective_page_title is pre-computed for you.

  ### SEO description cascade (what renders as the <meta description> tag)
    seo_description  →  short_description  →  item description  →  item_name
    effective_seo_description is pre-computed for you.

  ### Pricing cascade
    effective_price = sale_price (when sale_price_in_effect = true)  OR  price
    All amounts are in the currency field (USD or CAD).
    msrp and retail_price are informational (not always present).

  ### Multi-catalog pricing (catalog_pricing[])
    catalog_pricing lists pricing for all main WarmlyYours catalogs the SKU is in:
      catalog_id 1 → en-US  (warmlyyours.com, USD)
      catalog_id 2 → en-CA  (warmlyyours.com/en-CA, CAD)
      catalog_id 125 → EU   (European catalog, EUR)
    Each entry includes: price, effective_price, sale_price_in_effect, map_price,
    sale_price_effective_date, sale_price_expiration_date, product_stock_status,
    item_is_web_accessible, catalog_item_state.
    Use catalog_pricing to compare US vs CA pricing or check sale windows across markets.

  ### Availability flags — what controls whether a product is purchasable
    item_is_web_accessible  = true  → product IS listed on the website
    product_stock_status    = "InStock" or "OutOfStock"
    item_condition          = "new" | "refurbished" | "damaged"
    item_is_discontinued    = true → item record permanently discontinued
    store_item_is_discontinued = true → store item discontinued separately
    hide_from_feed          = true → excluded from Google Shopping / feeds

  ### catalog_item_state — the product lifecycle state machine
    Each CatalogItem has a `state` that controls visibility and workflow.
    item_is_web_accessible = true ONLY when ALL of:
      - item.is_discontinued = false
      - store_item.is_discontinued = false
      - catalog_item.state NOT IN hidden_states

    States and their meaning:
      active              — publicly listed AND purchasable. The goal state.
      active_hidden       — purchasable by direct URL but NOT in search/navigation.
                            Used for: kit components sold as part of a kit only,
                            recently decommissioned products still fulfilling orders,
                            trade-only items not shown publicly.
      pending_onboarding  — needs setup/upload in customer or retailer systems.
      pending_discontinue — flagged for removal; unboarding in progress (non-primary catalogs).
      pending_client_review — submitted to a retailer/client; awaiting their acceptance.
      require_vendor_update — update is required in the retailer's catalog (action needed).
      pending_vendor_update — update sent to retailer; awaiting propagation.
      discontinued        — permanently discontinued. Set by background job or explicit event.
                            Cannot be ordered. Can be reactivated via `activate` event.
      invalid_catalog_item — data quality issue (e.g. missing required field).
                             Must be corrected before reactivation.

    hidden_states (item_is_web_accessible = false):
      active_hidden, discontinued, pending_client_review,
      invalid_catalog_item, pending_onboarding

    Available events and valid transitions:
      activate                      discontinued → active
      hide_from_public              active → active_hidden
      unboard                       active → pending_discontinue
      discontinue                   most states → discontinued (main WY catalogs: 1,2,125)
                                    active → pending_discontinue (third-party catalogs)
      ready_for_onboarding          pending_client_review | discontinued → pending_onboarding
      items_are_onboarded           pending_onboarding → active
      submitted_to_client_for_review  invalid | discontinued | active → pending_client_review
      invalidate                    any → invalid_catalog_item
      item_specs_or_price_updated   active | pending_vendor_update → require_vendor_update
      submitted_updates_to_client   active | require_vendor_update → pending_vendor_update
      client_completed_updates      require_vendor_update | pending_vendor_update → active

    Common workflows:
      Make product public:        discontinued → activate → active
      Hide without discontinuing: active → hide_from_public → active_hidden
      Start wind-down (gentle):   active → unboard → pending_discontinue → discontinue → discontinued
      Direct discontinue (WY):    active → discontinue → discontinued
      Retailer onboarding:        active → submitted_to_client_for_review → pending_client_review
                                  → ready_for_onboarding → pending_onboarding
                                  → items_are_onboarded → active

  ### allow_add_to_cart
    true when: item_is_web_accessible = true AND restricted_to_trade = false
    restricted_to_trade = true for TempZone Ruler Cable heating cables
      (these are trade-only; show "Contact us for a quote" instead of cart)

  ### item_condition: refurbished
    refurbished_item = true means this IS a refurbished SKU (sku ends in "-BTK").
    has_new_version = true means there is a newer non-refurbished counterpart.
    Refurbished SKUs show a discount vs original_price.

  ### Item vs ProductLine hierarchy
    product_line  = the direct ProductLine the item belongs to (primary)
      primary_pl_path_slugs = ltree path e.g. "floor_heating.tempzone.flex_roll"
      Ancestors: floor_heating → tempzone → flex_roll
    product_category = the product category (PC) ltree path
      pc_path_slugs = ltree path e.g. "goods.heating_elements.heating_mats"
    show_in_sales_portal  — product line is shown in the public sales portal
    available_to_public   — product line is available to public

  ### Specifications
    specifications are pre-rendered, grouped by grouping (e.g. "Electrical", "Dimensions").
    Only "open_visibility" specs are shown publicly.
    Features (grouping = "Features" / "Highlights") are listed separately as features[].

  ### Variants
    Products in the same item_grouping_identifier are variants of each other
    (e.g. different sizes of the same mat). variants[] lists all SKUs with prices.

  ### Smart services (PC path starts with "services")
    These are professional installation services, not physical goods.
    They have terms_and_conditions and potential surcharges instead of a normal cart.

  ### Kit items
    item_is_kit = true → this SKU is a kit composed of multiple component items.

  ### Successor / discontinue
    successor_sku — the recommended replacement when this item is discontinued.

  ### Website images (images[])
    images[] contains WarmlyYours website image profiles (image_type starts with WYS_):
      WYS_CARD     — listing / product-card thumbnail (not on PDP carousel or feeds)
      WYS_MAIN     — main product image (also referenced as primary_image_url)
      WYS_I01–I15  — additional product images (gallery)
    Each entry has: image_type, url (full ImageKit CDN URL), locale.
    primary_image_url is the URL of the item's designated primary_image directly.

  ### Videos (videos[])
    videos[] are DigitalAsset records of type Video linked to this item.
    Each entry has: id, title, category (e.g. "installation", "product", "training"),
    cloudflare_uid (for Cloudflare Stream embed), duration_seconds, locales.
    Videos with cloudflare_uid are streamable; others may be external links.

  ### Publications (publications[])
    publications[] are linked Items of the "publication" type (PDFs, manuals,
    datasheets, installation guides). Each entry has: id, sku, title, category,
    locales (available language editions), url (download URL).
    These represent the technical documentation attached to this product.

  ### Model-specific support identification
    A family/model name is not an exact product identifier. When catalog evidence
    shows multiple configurations, ask for the exact label SKU or distinguishing
    configuration and wait before selecting documentation or parts. Never guess
    one candidate SKU.

  ### Mobility translations (translations{})
    Item text fields (name, seo_title, seo_description, short_description, etc.)
    support multiple locales via Mobility (container backend with locale_accessors).
    translations{} shows non-default locale values that have been entered.
      Default locale is 'en' (English). Fields in the default locale are
      returned directly as the top-level seo_title, name, etc. fields.
      Non-default locales appear under their locale key, e.g.:
        "fr-CA": { name: "...", seo_title: "..." }
    To EDIT a translated field, write to the locale-specific accessor, e.g.:
      Mobility.with_locale(:'fr-CA') { item.seo_title = "..." }
    Column accessors also exist (e.g. item.seo_title_fr_ca), but Mobility.with_locale
    is the safe, backend-agnostic approach and correctly triggers callbacks.
    _en suffix is the default; e.g. item.name_en is the English name.
    European products (EU catalog 125) may have fr, de, nl, es, it etc. locales.
    Always check translations{} to see what already exists before overwriting.
GUIDE
CONTENT_EDITING_GUIDE =

Guide injected into the content-editing tools (item + product-line text).

<<~GUIDE
  ## Product Content Editing Guide

  There are two distinct marketing-copy surfaces — pick the right one:

  ### Product-line descriptions  →  update_product_line
    The CRM `?tab=descriptions` editor edits ProductLine fields:
      description_html   — the main marketing description (rich HTML)
      short_description  — brief summary
      feature_1..feature_5, tag_line, name, public_name, seo_title,
      seo_description, seo_support_* … (see editable_fields in the response)
    A product-line description is INHERITED by every item in the line that
    has no item-level override — so this is the efficient way to change copy
    for a whole line at once.

  ### Item (SKU) descriptions  →  update_product
    Item-level fields, used when ONE SKU needs copy different from its line:
      detailed_description_html — the item's own long description (HTML)
      short_description, name, public_short_name, seo_title, seo_description,
      feature_1..feature_5 … (see editable_fields in the response)

  ### Locales
    Every field above is per-locale (Mobility). Pass `locale` to target one
    (default "en"). Common: en, en-CA, fr-CA, plus EU (fr, de, nl, es, it).
    Omit `locale` on the get_* tools to see every locale at once.

  ### Workflow — ALWAYS read before you write
    1. get_product_content / get_product_line_content — read the CURRENT copy
       for the locale you intend to edit.
    2. Weave new wording in ORGANICALLY and send the FULL revised field value.
       These tools REPLACE the field; they do NOT append.
    3. update_product / update_product_line to save.
    4. Pass dry_run: true first to preview before/after without saving.

  Note: the public site is CDN-cached — edits can take a few minutes (or the
  next cache purge / deploy) to appear on warmlyyours.com.
GUIDE
CATALOG_ITEM_STATE_EVENTS =

CatalogItem state-machine events fireable via update_catalog_item_state.

%w[
  activate hide_from_public unboard discontinue
  ready_for_onboarding items_are_onboarded
  submitted_to_client_for_review invalidate
  item_specs_or_price_updated submitted_updates_to_client
  client_completed_updates
].freeze

Class Method Summary collapse

Class Method Details

.management_tools(audit_context: {}) ⇒ Array<RubyLLM::Tool>

Write tools for product lifecycle + content management — require
item_manager role (gated by AssistantChatController#available_chat_services).

update_catalog_item_state — fire a CatalogItem state-machine event
get_product_content — read an item's editable text (all locales)
update_product — edit an item's text fields in a locale
get_product_line_content — read a product line's editable text
update_product_line — edit a product line's text fields in a locale

Parameters:

  • audit_context (Hash) (defaults to: {})

    must include :user_id

Returns:

  • (Array<RubyLLM::Tool>)


264
265
266
267
268
269
270
271
272
# File 'app/services/assistant/product_tool_builder.rb', line 264

def management_tools(audit_context: {})
  [
    build_update_catalog_item_state_tool(audit_context),
    build_get_product_content_tool,
    build_update_product_content_tool(audit_context),
    build_get_product_line_content_tool,
    build_update_product_line_content_tool(audit_context)
  ]
end

.toolsArray<RubyLLM::Tool>

Read-only product catalog tools — available to all authenticated users.

Returns:

  • (Array<RubyLLM::Tool>)


243
244
245
246
247
248
249
250
251
# File 'app/services/assistant/product_tool_builder.rb', line 243

def tools
  [
    build_list_product_lines_tool,
    build_get_product_tool,
    build_search_products_tool,
    build_browse_product_line_tool,
    build_get_heating_recommendation_tool
  ]
end