Ë
    §‘ˆj‹  ã                   óÖ   — d Z ddlZddlmZmZ ddlmZmZmZm	Z	 ddl
mZ ddlmZ  ej                  e«      Zh d£ZdZd	d
dœZdZdedefd„Zdedefd„Zdedededefd„Zdedefd„Zy)aA  Deciding whether a visitor's activity is worth a suggestion.

Deterministic rules and a database lookup, deliberately. This runs on every page
view of every visitor, so it must answer in tens of milliseconds; a model call
would take seconds and would spend the tenant's ai_tokens quota -- the same
budget the chatbot itself needs -- on visitors who never open the widget.

Neighbours come from the pairing graph (`servable_neighbours`), which is built
offline and already carries a real relationship (`pair_type`, `pair_score`)
rather than "same category". `related_products` (the `related_keys` column) is
the fallback for a tenant who has never run pairing -- it keeps that tenant no
worse off than before the graph existed, but it is not the primary source.

Nothing reachable from `decide()` calls a model. The `search` branch matches on
text (`match_products`, a plain ILIKE lookup) rather than on meaning -- it is
deliberately not `search_products`, which embeds the query and spends
ai_tokens quota. That embedding search is fine inside the chat tool, where a
model call is already in the loop and the visitor is waiting on a reply
anyway; it is not fine here, where the caller is an unauthenticated widget
firing on every page view. Do not swap this back to `search_products` to
"improve" match quality -- that would reintroduce a model call, and therefore
cost and latency, into a path that must stay a cheap lookup.

There is no cooldown in this version. Every qualifying event returns a
recommendation, which means a visitor moving through several pages is suggested
something on each one.
é    N)Ú	parse_qslÚurlparse)Úfind_by_urlÚmatch_productsÚrelated_productsÚto_card)Úget_tool_settings)Úservable_neighbours>   ÚdwellÚsearchÚ	page_viewÚadd_to_cartÚcart_abandonÚcheckout_starté   z*Still deciding? You might also like these.z Here's what matched your search.)r   r   )ÚqÚsÚqueryr   ÚkeywordÚkÚurlÚreturnc                 óØ   — 	 t        t        t        | «      j                  d¬«      «      }t
        D ](  }|j                  |«      sŒ||   j                  «       c S  y# t        $ r Y yw xY w)z=The search term a search results page carries in its own URL.F)Úkeep_blank_valuesÚ )Údictr   r   r   Ú	ExceptionÚ_SEARCH_PARAMSÚgetÚstrip)r   ÚparamsÚnames      úC/var/www/html/strategist-ai/app/services/pairing/recommendations.pyÚsearch_query_from_urlr$   6   sk   € ðÜ”i¤¨£× 3Ñ 3ÀuÔMÓNˆô ò (ˆØ�:‰:�dÕØ˜$‘<×%Ñ%Ó'Ò'ð(ð øô ò Ùðús   ‚)A Á	A)Á(A)Úreasonc                 ó   — d| dœS )NF)Ú	recommendr%   © )r%   s    r#   Ú_nothingr)   B   s   € Ø¨&Ñ1Ð1ó    Ú	tenant_idÚeventÚpagec                 óp  — 	 t        | «      d   }|j                  dd«      st        d«      S t        | |j                  d«      xs d«      }|st        d«      S |dk(  r'|j                  d	«      xs d
t        k  rt        d«      S |dk7  rt        d«      S |dk(  r:t        |j                  d«      xs d«      }|st        d«      S t        | |d¬«      }n!t        | |d   d¬«      }|st        | |d¬«      }|D �cg c]'  }|j                  d«      |j                  d«      k7  r|‘Œ) c}dd }|st        d«      S dt        j                  |t        d   «      |D �cg c]  }t        |«      ‘Œ c}dœS c c}w c c}w # t        $ r2}	t        j                  d| › d|	› �d¬«       t        d«      cY d}	~	S d}	~	ww xY w)z4Whether to suggest anything, and what. Never raises.ÚfeaturesÚproduct_recommendationTÚfeature_offr   r   Úpage_unknownr   Údwell_secondsr   Úno_rule_matchedr   é   )ÚlimitÚproduct_keyNé   Úno_match)r'   ÚmessageÚproductsz#Recommendation decision failed for z: )Úexc_info)r	   r   r)   r   ÚDWELL_THRESHOLD_SECONDSr$   r   r
   r   ÚMESSAGESÚ_attributedr   ÚloggerÚerror)
r+   r,   r-   r/   Úcurrentr   Ú
candidatesÚrÚothersÚexs
             r#   ÚdeciderG   F   sÂ  € ð+$Ü$ YÓ/°
Ñ;ˆØ�|‰|Ð4°dÔ;Ü˜MÓ*Ð*ä˜i¨¯©°%«Ò)>¸BÓ?ˆÙÜ˜NÓ+Ð+à�GÒØ—‘˜Ó)Ò.¨QÔ2IÒIÜÐ 1Ó2Ð2Ø�hÒô Ð-Ó.Ð.à�HÒÜ)¨$¯(©(°5«/Ò*?¸RÓ@ˆEÙÜÐ 1Ó2Ð2Ü'¨	°5ÀÔB‰Jô
 -¨Y¸ÀÑ8NØ34ô6ˆJáÜ-¨i¸ÈÔJ�
ð (ö I˜Ø—U‘U˜=Ó)¨W¯[©[¸Ó-GÒGò ò IØIKÈ!ðMˆáÜ˜JÓ'Ð'à!Ü#Ÿ<™<¨¬x¸Ñ/@ÓAØ5;Ö<°œ[¨�^Ò<ñ>ð 	>ùòIùò =øÜò $ä�‰Ð:¸9¸+ÀRÈÀtÐLÐW[ˆÔ\Ü˜
Ó#Õ#ûð$ús^   ‚*E: ­+E: Á+E: ÂE: Â/E: Ã4E: Ã9,E0Ä%E: Ä8!E: ÅE5Å+E: Å0
E: Å:	F5Æ'F0Æ*F5Æ0F5Úrowc                 óv   — t        | «      }| j                  d«      r| d   |d<   | j                  d«      |d<   |S )z³to_card is shared with the chat path and must not learn about pairing, so
    the attribution is attached here. Phase 5 needs it to work out which pair
    types earn their place.Ú	pair_typeÚ
pair_score)r   r   )rH   Úcards     r#   r?   r?   v   s@   € ô �3‹<€DØ
‡w�wˆ{ÔØ Ñ,ˆˆ[ÑØ ŸW™W \Ó2ˆˆ\ÑØ€Kr*   )Ú__doc__ÚloggingÚurllib.parser   r   Úapp.services.catalog.productsr   r   r   r   Ú app.services.chat.tools_settingsr	   Úapp.services.pairing.queriesr
   Ú	getLoggerÚ__name__r@   ÚVALID_EVENTSr=   r>   r   Ústrr$   r   r)   rG   r?   r(   r*   r#   ú<module>rW      s±   ðñó6 ß ,ç `Ó `Ý >Ý <à	ˆ×	Ñ	˜8Ó	$€òA€ð Ð ð :Ø0ñ€ð ?€ð	˜sð 	 só 	ð2�Sð 2˜Tó 2ð-$�cð -$ #ð -$¨Tð -$°dó -$ð`�Tð ˜dô r*   