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 =
"#{CRM_URL}/en-US/items".freeze
WEB_PRODUCT_URL =
"https://www.warmlyyours.com".freeze
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.

  ### 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

Class Method Summary collapse

Class Method Details

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

Write tools for product lifecycle management — require item_manager role.

Parameters:

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

    must include :user_id

Returns:

  • (Array<RubyLLM::Tool>)


197
198
199
200
201
# File 'app/services/assistant/product_tool_builder.rb', line 197

def management_tools(audit_context: {})
  [
    build_update_catalog_item_state_tool(audit_context)
  ]
end

.toolsArray<RubyLLM::Tool>

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

Returns:

  • (Array<RubyLLM::Tool>)


184
185
186
187
188
189
190
191
192
# File 'app/services/assistant/product_tool_builder.rb', line 184

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