Ë
    qýujo>  ã            
       ó4  — d Z ddlZddlZddlmZmZmZmZ ddlm	Z	 ddl
mZmZ ddlmZ ddlmZ ddlmZmZmZ dd	lmZ dd
lmZmZ  ej4                  e«      Zh d£Z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"de%f
d„Z&de de de'de(de$f
d„Z)de de de%fd„Z*de de$de$fd„Z+d2de de de(de$fd„Z,de d e$de$fd!„Z-d3de d"e de(de$fd#„Z.d3de d"e de(de$fd$„Z/d3de d%e%de(de$fd&„Z0d'e$d(e$de1fd)„Z2d4d*e$de d+e de$fd,„Z3d5de de(d+e de(fd-„Z4de d.e d/e$ddfd0„Z5d%e%de%fd1„Z6y)6zÈStoring and querying the tenant's products.

No parsing here -- that is product_extraction. This module owns the table, the
URL matching rule, and the shape products take when they leave the backend.
é    N)Ú	parse_qslÚ	urlencodeÚurlparseÚ
urlunparse)Úsql)ÚJsonÚRealDictCursor)Úsettings)Úclient)Úget_db_connectionÚinsert_llm_usageÚsearch_vector_data)Úget_embeddings)Úcheck_quotaÚestimate_text_tokens>   ÚrefÚgclidÚfbclidÚmc_cidÚmc_eidÚutm_idÚmsclkidÚreferrerÚutm_termÚ
utm_mediumÚ
utm_sourceÚutm_contentÚutm_campaigné@   iô  ÚurlÚreturnc           	      ó€  — t        | xs d«      j                  «       } | syt        | «      }t        |j                  d¬«      D ��cg c]  \  }}|j                  «       t        vr||f‘Œ! }}}|j                  j                  d«      xs d}t        |j                  |j                  |dt        |«      df«      S c c}}w )aâ  Make two spellings of the same page match.

    Strips the fragment, a trailing slash, and known tracking parameters. Keeps
    every other query parameter: plenty of sites identify the product there
    (?product_id=123), and dropping the query wholesale would collapse a whole
    catalogue onto one URL.

    Tolerant of non-string input (e.g. None): callers that pass a bad value get
    "" back rather than an AttributeError, so lookups return empty rather than
    raise.
    Ú T)Úkeep_blank_valuesú/)ÚstrÚstripr   r   ÚqueryÚlowerÚ_TRACKING_PARAMSÚpathÚrstripr   ÚschemeÚnetlocr   )r    ÚpartsÚkÚvr(   r+   s         ú4/var/www/html/strategist-ai/app/services/products.pyÚnormalise_urlr3   #   sª   € ô ˆcŠi�R‹.×
Ñ
Ó
 €CÙØÜ�S‹M€EÜ )¨%¯+©+ÈÔ N÷ 3™˜˜1Ø—‘“	Ô!1Ñ1ð �ŠVð 3€Eñ 3à�:‰:×Ñ˜SÓ!Ò( S€DÜ�u—|‘| U§\¡\°4¸¼YÀuÓ=MÈrÐRÓSÐSùó3s   Á$B:Ú	tenant_idc                 óš   — | j                  «       5 }|j                  d|f«       |j                  «       d   cddd«       S # 1 sw Y   yxY w)a¬  Whether the tenant has a products table.

    Opens its own plain cursor rather than taking the caller's, because
    callers use different cursor factories (`save_products` and
    `save_related` use a plain cursor, `_query` uses a `RealDictCursor`) and a
    row read as `row[0]` breaks under a dict cursor while `row["exists"]`
    breaks under a plain one. A private cursor keeps this function correct
    under either.
    ztSELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = %s AND table_name = 'strategist_products')r   N)ÚcursorÚexecuteÚfetchone)Úconnr4   Úcurs      r2   Ú_table_existsr;   9   sK   € ð 
�‰‹ð !˜#Ø�‰ðNàˆLô	ð �|‰|‹~˜aÑ ÷!÷ !ò !ús   ‘&AÁA
ÚproductsÚcrawled_urlsÚcrawl_completec                 ó  — dx}}t        «       }	 |j                  «       5 }t        || «      s7t        j	                  d| › d�«       dddœcddd«       |j                  «        S |D ]ê  }|j                  t        j                  d«      j                  t        j                  | «      «      |d   |d   |j                  d	«      |j                  d
«      t        |d   «      |j                  d«      t        |j                  d«      xs g «      t        |j                  d«      xs g «      t        |j                  d«      xs i «      f	«       |dz  }Œì |rm|rk|j                  t        j                  d«      j                  t        j                  | «      «      |D �	cg c]  }	t        |	«      ‘Œ c}	f«       |j                  }ddd«       |j                  «        t        j!                  d| › d|› d|› d|› d�	«       ||dœ|j                  «        S c c}	w # 1 sw Y   ŒUxY w# t"        $ rL}
|j%                  «        t        j'                  d| › d|
› �d¬«       dddœcY d}
~
|j                  «        S d}
~
ww xY w# |j                  «        w xY w)a  Upsert products, and delete stale ones only when it is safe to.

    A crawl that failed halfway, or that was truncated by max_pages, would
    otherwise delete products that are still on sale. When `crawl_complete` is
    false, this writes and updates but deletes nothing.
    r   zNo products table for z; skipping save.)ÚsavedÚdeletedNa@  
                    INSERT INTO {}.strategist_products
                        (product_key, name, description, image_url, product_url,
                         category, ctas, options, raw, extracted_at)
                    VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s, CURRENT_TIMESTAMP)
                    ON CONFLICT (product_key) DO UPDATE SET
                        name = EXCLUDED.name,
                        description = EXCLUDED.description,
                        image_url = EXCLUDED.image_url,
                        product_url = EXCLUDED.product_url,
                        category = EXCLUDED.category,
                        ctas = EXCLUDED.ctas,
                        options = EXCLUDED.options,
                        raw = EXCLUDED.raw,
                        extracted_at = CURRENT_TIMESTAMP
                Úproduct_keyÚnameÚdescriptionÚ	image_urlÚproduct_urlÚcategoryÚctasÚoptionsÚrawé   z?DELETE FROM {}.strategist_products WHERE product_url <> ALL(%s)zProducts for ú: z saved, z deleted (crawl_complete=ú)zCould not save products for T©Úexc_info)r   r6   r;   ÚloggerÚwarningÚcloser7   r   ÚSQLÚformatÚ
IdentifierÚgetr3   r   ÚrowcountÚcommitÚinfoÚ	ExceptionÚrollbackÚerror)r4   r<   r=   r>   r@   rA   r9   r:   ÚproductÚuÚexs              r2   Úsave_productsr`   K   sR  € ð Ð€EˆGÜÓ€Dð-Ø�[‰[‹]ð "	'˜cÜ   yÔ1Ü—‘Ð!7¸	°{ÐBRÐSÔTØ!"¨qÑ1÷"	'ð "	'ðX 	�
‰
�ðO $ò �Ø—‘œCŸG™Gð %ó ÷ ‘VœCŸN™N¨9Ó5Ó6Ø˜MÑ*¨G°F©O¸W¿[¹[ÈÓ=WØ—K‘K Ó,¬m¸GÀMÑ<RÓ.SØ—K‘K 
Ó+¬T°'·+±+¸fÓ2EÒ2KÈÓ-LÜ˜Ÿ™ YÓ/Ò5°2Ó6¼¸W¿[¹[ÈÓ=OÒ=UÐSUÓ8Vð	9ôð* ˜‘
‘ð-ñ0 ¡,Ø—‘œCŸG™GØUóç‘&œŸ™¨	Ó2Ó3Ø0<Ö=¨1”m AÕ&Ò=Ð?ôAð Ÿ,™,�÷E"	'ðF 	�‰ŒÜ�‰�m I ;¨b°°°xÀ¸yð I'Ø'5Ð&6°að9ô 	:à¨7Ñ3ð 	�
‰
�ùò >÷C"	'ð "	'ûôN ò *Ø�‰ŒÜ�‰Ð3°I°;¸bÀÀÐEÐPTˆÔUØ qÑ)Ô)à�
‰
�ûð*ûð
 	�
‰
�úse   �H"  *HÁ
	H" Á$D8HÆHÆ.HÇ>H" ÈHÈHÈH" È"	I7È+1I2ÉI7ÉI: É2I7É7I: É:JÚwhereÚparamsÚlimitc                 ó²  — t        «       }	 |j                  t        ¬«      5 }t        || «      sg cd d d «       |j	                  «        S |j                  t        j                  d|z   dz   «      j                  t        j                  | «      «      ||fz   «       |j                  «       D �cg c]  }t        |«      ‘Œ c}cd d d «       |j	                  «        S c c}w # 1 sw Y   nxY wnE# t        $ r9}t        j                  d| › d|› �d¬«       g cY d }~|j	                  «        S d }~ww xY w	 |j	                  «        y # |j	                  «        w xY w)N©Úcursor_factoryz+SELECT * FROM {}.strategist_products WHERE z$ ORDER BY extracted_at DESC LIMIT %szProduct query failed for rL   TrN   )r   r6   r	   r;   rR   r7   r   rS   rT   rU   ÚfetchallÚdictrZ   rP   r\   )r4   ra   rb   rc   r9   r:   Úrr_   s           r2   Ú_queryrj   …   s*  € ÜÓ€DðØ�[‰[¬ˆ[Ó7ð 	5¸3Ü   yÔ1Ø÷	5ð 	5ð 	�
‰
�ð �K‰KœŸ™Ø=ÀÑEØ6ñ7ó÷ ‰f”S—^‘^ IÓ.Ó/°¸5¸(Ñ1BôDð &)§\¡\£^Ö4 ”D˜•GÒ4÷	5ð 	5ð 	�
‰
�ùò 5÷	5ð 	5úð 	5øô ò Ü�‰Ð0°°¸2¸b¸TÐBÈTˆÔRØŒ	à�
‰
�ûð	úð	5ð 	�
‰
�øˆ�
‰
�úsj   ŒC- ¢C °	C- Á
A#C Â-CÂ?C Ã	C- ÃC Ã C)Ã%C- Ã,E Ã-	D/Ã6D*ÄD/ÄE Ä*D/Ä/E ÅEc                 óD   — t        | dt        |«      fd«      }|r|d   S i S )Nzproduct_url = %srK   r   )rj   r3   )r4   r    Úrowss      r2   Úfind_by_urlrm   —   s,   € Ü�)Ð/´-ÀÓ2DÐ1FÈÓJ€DÙˆ4�‰7Ð" Ð"ó    Úurlsc           
      ór   — |sg S t        | d|D �cg c]  }t        |«      ‘Œ c}ft        |«      «      S c c}w )Nzproduct_url = ANY(%s))rj   r3   Úlen)r4   ro   r^   s      r2   Úfind_by_urlsrr   œ   s=   € ÙØˆ	Ü�)Ð4Ø.2Ö3¨”M !Õ$Ò3Ð5´s¸4³yóBð BùÚ3s   �4rG   c                 óB   — d\  }}|r|dz  }||fz  }t        | |||«      S )N)ÚTRUE© z  AND lower(category) = lower(%s))rj   )r4   rG   rc   ra   rb   s        r2   Úlist_productsrv   £   s6   € Ø�M€Eˆ6ÙØÐ3Ñ3ˆØ�8�+ÑˆÜ�)˜U F¨EÓ2Ð2rn   Úkeysc                 óL   — |sg S t        | dt        |«      ft        |«      «      S )Nzproduct_key = ANY(%s))rj   Úlistrq   )r4   rw   s     r2   Úfind_by_keysrz   «   s&   € ÙØˆ	Ü�)Ð4´t¸D³z°mÄSÈÃYÓOÐOrn   r(   c                 ó<  — |xs dj                  «       }|sg S 	 t        || ¬«      }t        | |d¬«      }g t        «       }}|xs g D ]]  }t        |j                  d
«      xs i j                  d«      xs d«      }	|	sŒ7|	|vsŒ<|j                  |	«       |j                  |	«       Œ_ t        | |«      D �
ci c]  }
|
d   |
“Œ
 }}
|D �cg c]  }||v sŒ||   ‘Œ c}d	| S # t        $ r)}t        j                  d| › d|› �d¬«       g cY d	}~S d	}~ww xY wc c}
w c c}w )až  Products matching what the visitor actually meant.

    The product pages are already chunked and embedded in the knowledge base, so
    the search happens there and the winning chunks' source_url is mapped back
    onto products. This is a semantic match: "something light for jogging" finds
    a running shoe whose description uses neither word, where a category or
    keyword filter would return nothing.
    r#   )r4   é   )rc   zProduct search failed for rL   TrN   NÚmetadataÚ
source_urlrF   )r'   r   r   rZ   rP   r\   Úsetr3   rV   ÚaddÚappendrr   )r4   r(   rc   Ú	embeddingÚhitsr_   ro   ÚseenÚhitr    ri   rl   r^   s                r2   Úsearch_productsr†   ±   s*  € ð Š[�b×ÑÓ!€EÙØˆ	ðÜ" 5°IÔ>ˆ	Ü! )¨Y¸bÔAˆð ”S“Uˆ$€DØŠz�rò ˆÜ˜SŸW™W ZÓ0Ò6°B×;Ñ;¸LÓIÒOÈRÓPˆÚ�3˜d’?Ø�H‰H�SŒMØ�K‰K˜Õð	ô *6°iÀÓ)FÖG AˆAˆmÑ˜aÑÐG€DÐGØ!Ö/˜ Q¨$¢YˆD�‹GÒ/°°Ð7Ð7øô ò Ü�‰Ð1°)°¸B¸r¸dÐCÈdˆÔSØ�	ûðüò HùÚ/s/   šC Â5DÃ	DÃDÃ	DÃ(DÄDÄDc           
      óæ  — |xs dj                  «       }|sg S t        «       }	 |j                  t        ¬«      5 }t	        || «      sg cddd«       |j                  «        S d|› d�}|j                  t        j                  d«      j                  t        j                  | «      «      ||||||f«       |j                  «       D �cg c]  }t        |«      ‘Œ c}cddd«       |j                  «        S c c}w # 1 sw Y   nxY wnE# t        $ r9}t        j                  d| › d|› �d¬	«       g cY d}~|j                  «        S d}~ww xY w	 |j                  «        y# |j                  «        w xY w)
a  Products whose text plainly contains the visitor's search term.

    A single ILIKE query against name, description and category, ordered so a
    name match outranks a description match and a description match outranks a
    category match. No embedding call and no quota check: unlike
    `search_products`, this is meant to run on every page view of every
    visitor, so it has to be a deterministic database lookup rather than a
    model round-trip. Never raises -- `_query` already returns [] on failure.
    r#   re   Nú%a�  
                SELECT *, CASE
                    WHEN name ILIKE %s THEN 0
                    WHEN description ILIKE %s THEN 1
                    ELSE 2
                END AS _rank
                FROM {}.strategist_products
                WHERE name ILIKE %s OR description ILIKE %s OR category ILIKE %s
                ORDER BY _rank, extracted_at DESC
                LIMIT %s
            zProduct text match failed for rL   TrN   )r'   r   r6   r	   r;   rR   r7   r   rS   rT   rU   rg   rh   rZ   rP   r\   )r4   r(   rc   r9   r:   Úlikeri   r_   s           r2   Úmatch_productsrŠ   Ð   sO  € ð Š[�b×ÑÓ!€EÙØˆ	ÜÓ€DðØ�[‰[¬ˆ[Ó7ð 	5¸3Ü   yÔ1Ø÷	5ð 	5ð* 	�
‰
�ð% �u�g˜Q�<ˆDØ�K‰KœŸ™ð 
!ó 
÷ ‘œŸ™ yÓ1Ó2Ø�t˜T 4¨¨uÐ5ô7ð &)§\¡\£^Ö4 ”D˜•GÒ4÷!	5ð 	5ð* 	�
‰
�ùò 5÷!	5ð 	5úð 	5øô" ò Ü�‰Ð5°i°[ÀÀ2À$ÐGÐRVˆÔWØŒ	à�
‰
�ûð	úð#	5ð* 	�
‰
�øˆ�
‰
�úsk   ¤D ºC:Á	D Á"A%C:ÃC5ÃC:Ã	D Ã5C:Ã:DÃ?D ÄE Ä	E	ÄEÄ.E	Ä/E ÅE	Å	E ÅE0Úrowc                 óÊ   — t        |j                  d«      xs g «      d| }|sg S t        | |«      D �ci c]  }|d   |“Œ
 }}|D �cg c]  }||v sŒ||   ‘Œ c}S c c}w c c}w )z»A product's nearest neighbours, computed at ingestion.

    A lookup rather than a similarity search, because this runs on every page
    view and the thinking has already been done.
    Úrelated_keysNrB   )ry   rV   rz   )r4   r‹   rc   rw   ri   Úfoundr0   s          r2   Úrelated_productsr�   ÷   ss   € ô �—‘˜Ó'Ò-¨2Ó.¨v°Ð6€DÙØˆ	Ü*6°yÀ$Ó*GÖH QˆQˆ}Ñ˜qÑ ÐH€EÐHØ"Ö1˜ a¨5¢jˆE�!‹HÒ1Ð1ùò IùÚ1s   ´AÁ	A ÁA ÚaÚbc                 óª   — t        d„ t        | |«      D «       «      }t        d„ | D «       «      dz  }t        d„ |D «       «      dz  }|r
|r|||z  z  S dS )zASimilarity between two embeddings, 0.0 when either is degenerate.c              3   ó,   K  — | ]  \  }}||z  –— Œ y ­w©Nru   )Ú.0ÚxÚys      r2   ú	<genexpr>z_cosine.<locals>.<genexpr>  s   è ø€ Ò*™˜˜1ˆa�!�eÑ*ùs   ‚c              3   ó&   K  — | ]	  }||z  –— Œ y ­wr”   ru   )r•   r–   s     r2   r˜   z_cosine.<locals>.<genexpr>  ó   è ø€ Ò�qˆQ��UÑùó   ‚g      à?c              3   ó&   K  — | ]	  }||z  –— Œ y ­wr”   ru   )r•   r—   s     r2   r˜   z_cosine.<locals>.<genexpr>  rš   r›   g        )ÚsumÚzip)r�   r‘   ÚdotÚnaÚnbs        r2   Ú_cosiner¢     sY   € ä
Ñ*¤ A q£	Ô*Ó
*€CÜ	Ñ˜AÔÓ	 #Ñ	%€BÜ	Ñ˜AÔÓ	 #Ñ	%€BÙ ¡Rˆ3�"�r‘'‰?Ð0¨SÐ0rn   ÚtextsÚuser_idc           	      ó®  — g }	 t        dt        | «      t        «      D ]ÿ  }| ||t        z    }|r!t        d„ |D «       «      }t	        |d||¬«       t
        j                  j                  |t        j                  d¬«      }|j                  }t        j                  dt        |«      › d|j                  › d	|j                  › �«       |r2t        |d
t        j                  |j                  d|j                  «       |j!                  d„ |j"                  D «       «       �Œ |S # t$        $ r)}	t        j'                  d|› d|	› �d¬«       g cY d}	~	S d}	~	ww xY w)a®  Embed many texts in as few API calls as possible, in input order.

    One call per _EMBED_BATCH_SIZE texts instead of one call per text, so
    computing relatedness for a real catalogue is dozens of round-trips rather
    than thousands. Usage is logged once per batch, the same way
    `get_embeddings` logs once per call. Never raises: any failure returns []
    and the caller treats that as "skip relatedness this run".
    r   c              3   ó2   K  — | ]  }t        |«      –— Œ y ­wr”   )r   )r•   Úts     r2   r˜   z_embed_batch.<locals>.<genexpr>  s   è ø€ ÒG¸AÔ 4°Q× 7ÑGùs   ‚Ú	ai_tokens)r¤   Úrequested_amountg      >@)ÚinputÚmodelÚtimeoutzLLM Usage (Embedding batch of z): Prompt: z	, Total: Ú	Embeddingc              3   ó4   K  — | ]  }|j                   –— Œ y ­wr”   )r‚   )r•   Úitems     r2   r˜   z_embed_batch.<locals>.<genexpr>%  s   è ø€ ÒD¨d˜4Ÿ>�>ÑDùs   ‚zBatch embedding failed for rL   TrN   N)Úrangerq   Ú_EMBED_BATCH_SIZEr�   r   r   Ú
embeddingsÚcreater
   ÚEMBED_MODELÚusagerP   rY   Úprompt_tokensÚtotal_tokensr   ÚextendÚdatarZ   r\   )
r£   r4   r¤   ÚvectorsÚstartÚbatchÚ	requestedÚresponserµ   r_   s
             r2   Ú_embed_batchr¿     sK  € ð €GðÜ˜1œc %›jÔ*;Ó<ò 	EˆEØ˜% Ô(9Ñ 9Ð:ˆEÙÜÑGÀÔGÓG�	Ü˜I {¸GÐV_Õ`ä×(Ñ(×/Ñ/Ø¤8×#7Ñ#7Àð 0ó GˆHà—N‘NˆEÜ�K‰KÐ8¼¸U»¸ð E#Ø#(×#6Ñ#6Ð"7°yÀ×ASÑASÐ@TðVô WáÜ  ¨K¼×9MÑ9MØ!&×!4Ñ!4°a¸×9KÑ9KôMà�N‰NÑD°h·m±mÔDÖDð	Eð ˆøÜò Ü�‰Ð2°9°+¸RÀ¸tÐDÈtˆÔTØ�	ûðús   „DD" Ä"	EÄ+EÅ	EÅEc                 óð  ‡
‡‡— t        | ddd«      Št        ‰«      dk  ryt        ‰«      t        kD  r=t        ‰«      t        z
  }‰dt         Št        j	                  | › d|› dt        › d	�«       ‰D �cg c]-  }|d
   › d|j                  d«      xs d› �j                  «       ‘Œ/ }}t        || |¬«      Št        ‰«      t        ‰«      k7  rt        j                  d| › �«       yd}t        ‰«      D ]Z  \  Š
}t        ˆ
ˆˆfd„t        t        ‰«      «      D «       d¬«      }t        | |d   |d| D ��	cg c]  \  }}	|	‘Œ	 c}	}«       |dz  }Œ\ t        j                  d|› d| › �«       |S c c}w c c}	}w )a  Work out each product's nearest neighbours, once, at ingestion.

    Category matching is a reflex: it cannot tell that a running shoe goes with
    running socks but not a running-themed poster, and it fails outright on the
    many products that carry no category at all. Comparing what the products
    actually say does better.

    Doing it here rather than per request is what lets the proactive endpoint stay
    a fast lookup while still being a real recommendation. Never raises.

    Capped at MAX_RELATED_PRODUCTS (the newest rows -- `_query` already orders
    by extracted_at DESC): a catalogue larger than that would otherwise turn a
    single ingestion request into thousands of sequential embedding calls and
    an O(n^2) comparison. Skipping the rest is logged, not silent.
    rt   ru   i'  é   r   Nz has z products beyond the z(-product relatedness cap; skipping them.rC   z. rD   r#   )r4   r¤   zCould not embed products for c              3   ó\   •K  — | ]#  }|‰k7  rt        ‰‰   ‰|   «      ‰|   d    f–— Œ% y­w)rB   N)r¢   )r•   ÚjÚirl   rº   s     €€€r2   r˜   z&compute_relatedness.<locals>.<genexpr>O  s<   øè ø€ ò 2Ø¨!¨qª&ô �g˜a‘j '¨!¡*Ó-¨t°A©w°}Ñ/EÔFñ 2ùs   ƒ),T)ÚreverserB   rK   zComputed relatedness for z products in )rj   rq   ÚMAX_RELATED_PRODUCTSrP   rQ   rV   r'   r¿   r\   Ú	enumerateÚsortedr°   Úsave_relatedrY   )r4   rc   r¤   Úskippedr‹   r£   ÚupdatedÚscoredÚ_r0   rÄ   rl   rº   s             @@@r2   Úcompute_relatednessrÎ   ,  s†  ú€ ô  �)˜V R¨Ó/€DÜ
ˆ4ƒy�1‚}Øä
ˆ4ƒyÔ'Ò'Ü�d“)Ô2Ñ2ˆØÐ)Ô)Ð*ˆÜ�‰˜)˜ E¨'¨Ð2GÜ.Ð/Ð/WðYô 	Zð RVÖVÈ#��F‘ˆ}˜B˜sŸw™w }Ó5Ò;¸Ð<Ð=×CÑCÕEÐV€EÐVÜ˜5¨I¸wÔG€GÜ
ˆ7ƒ|”s˜4“yÒ Ü�‰Ð4°Y°KÐ@ÔAØà€GÜ˜D“/ò ‰ˆˆ3Üõ2ÜœC ›IÓ&ô2àôˆô 	�Y  MÑ 2À6È&È5À>×4R¹4¸1¸a²QÓ4RÔSØ�1‰‰ðô ‡K�KÐ+¨G¨9°MÀ)ÀÐMÔNØ€Nùò! Wùó 5Ss   Á42E-Ä5E2rB   Úrelatedc                 ó@  — t        «       }	 |j                  «       5 }t        || «      s	 ddd«       |j                  «        y|j	                  t        j                  d«      j                  t        j                  | «      «      t        |«      |f«       ddd«       |j                  «        |j                  «        y# 1 sw Y   Œ*xY w# t        $ r5}|j                  «        t        j                  d|› d|› �«       Y d}~ŒVd}~ww xY w# |j                  «        w xY w)z3Store a product's nearest neighbours. Never raises.NzJUPDATE {}.strategist_products SET related_keys = %s WHERE product_key = %sz$Could not save related products for rL   )r   r6   r;   rR   r7   r   rS   rT   rU   ry   rX   rZ   r[   rP   r\   )r4   rB   rÏ   r9   r:   r_   s         r2   rÉ   rÉ   Y  sç   € äÓ€DðØ�[‰[‹]ð 	O˜cÜ   yÔ1Ø÷	Oð 	�
‰
�ð �K‰KœŸ™Ø\óç‰f”S—^‘^ IÓ.Ó/´$°w³-ÀÐ1MôO÷	Oð 	�‰Œð
 	�
‰
�÷	Oð 	Oûô ò QØ�‰ŒÜ�‰Ð;¸K¸=ÈÈ2È$ÐO×PÑPûðQûð 	�
‰
�úsL   ŒC
 œB>ªC
 ÁAB>ÂC
 Â>CÃC
 Ã
	DÃ+DÃ>D ÄDÄD ÄDc                 ó”  — | j                  d«      xs g }t        |t        «      rt        j                  |«      }| j                  d«      xs g }t        |t        «      rt        j                  |«      }| j                  d«      | j                  d«      | j                  d«      | j                  d«      | j                  d«      ||dœS )	aM  The shape a product takes when it leaves the backend.

    Carries no price, currency or stock level: those go stale fastest between
    crawls, and a wrong price on a card shown to a customer is the worst thing
    this feature could do. The CTA takes the visitor to the page, where those
    numbers are correct by definition.
    rH   rI   rB   rC   rD   rE   rF   )Ú
product_idrC   rD   rE   r    rH   rI   )rV   Ú
isinstancer&   ÚjsonÚloads)r‹   rH   rI   s      r2   Úto_cardrÖ   k  s¤   € ð �7‰7�6‹?Ò ˜b€DÜ�$œÔÜ�z‰z˜$ÓˆØ�g‰g�iÓ Ò& B€GÜ�'œ3ÔÜ—*‘*˜WÓ%ˆà—g‘g˜mÓ,Ø—‘˜“Ø—w‘w˜}Ó-Ø—W‘W˜[Ó)Ø�w‰w�}Ó%ØØñð rn   )Né   )r×   )NN)é   N)7Ú__doc__rÔ   ÚloggingÚurllib.parser   r   r   r   Úpsycopg2r   Úpsycopg2.extrasr   r	   Úapp.core.configr
   Úapp.core.llm_clientr   Úapp.services.databaser   r   r   Úapp.services.embeddingr   Úapp.services.quotasr   r   Ú	getLoggerÚ__name__rP   r*   r±   rÆ   r&   r3   Úboolr;   ry   rh   r`   ÚtupleÚintrj   rm   rr   rv   rz   r†   rŠ   r�   Úfloatr¢   r¿   rÎ   rÉ   rÖ   ru   rn   r2   ú<module>ré      sD  ðñó
 Û ß CÓ Cå ß 0å $Ý &ß YÑ YÝ 1ß Aà	ˆ×	Ñ	˜8Ó	$€ò;Ð ð
 Ð ð
 Ð ðT�sð T˜só Tð,! 3ð !¨4ó !ð$7˜Sð 7¨Dð 7Àð 7Ø"&ð7Ø+/ó7ðt�cð  #ð ¨uð ¸Sð ÀTó ð$#˜3ð # Sð #¨Tó #ð
B˜Cð B tð B°ó Bñ3˜Sð 3¨Cð 3¸sð 3È4ó 3ðP˜Cð P tð P°ó Pñ8˜sð 8¨3ð 8°sð 8À4ó 8ñ>$˜cð $¨#ð $°cð $À$ó $ñN
2 ð 
2¨$ð 
2°sð 
2À4ó 
2ð1ˆtð 1˜ð 1 ó 1ñ˜ð ¨ð ¸cð ÈTó ñ@* 3ð *¨sð *Àð *ÐPSó *ðZ˜Cð ¨cð ¸Dð ÀTó ð$�ð ˜$ô rn   