Ë
    §‘ˆj  ã                   ó&  — d Z ddlZddlZddlmZ ddlmZmZ ddlm	Z	m
Z
 ddlmZ ddlmZmZ ddlmZmZ  ej&                  e«      Zd	Zd
Zdedefd„Zdededefd„Zdedefd„Zdedededdfd„Z dededdfd„Z!de"deddfd„Z#ddededefd„Z$y)aS  Runs attribute extraction across a tenant's catalog.

Selection is driven by content_hash: a product is re-extracted only when its
content actually changed. That is the real cost control -- the matching
specification's quality-score gate was measured at a 0.35 median on live data,
so it would catch nearly everything and control nothing.
é    N)Úsql)ÚJsonÚRealDictCursor)Ú
BATCH_SIZEÚextract_batch)Úmerge_attributes)Úget_db_connectionÚmigrate_products_table)ÚclassifyÚcompute_bandsé   zno-content-hashÚproductÚreturnc                 ó�   — | j                  d«      ry| j                  d«      xs d}t        |j                  «       «      t        k\  S )zRNo prompt fixes an absent input, so do not pay for a call that can only
    guess.ÚdescriptionTÚnameÚ )ÚgetÚlenÚsplitÚMIN_TEXT_WORDS)r   r   s     ú:/var/www/html/strategist-ai/app/services/enrichment/job.pyÚis_extractabler      s<   € ð ‡{�{�=Ô!ØØ�;‰;�vÓÒ$ "€DÜˆt�z‰z‹|Ó¤Ñ.Ð.ó    Ú	tenant_idÚforcec                 óì  — |rdnd}|rdnt         f}t        «       }	 |j                  t        ¬«      5 }|j	                  t        j                  d|z   «      j                  t        j                  | «      «      |«       |j                  «       D �cg c]  }t        |«      ‘Œ c}cd d d «       |j                  «        S c c}w # 1 sw Y   nxY w	 |j                  «        y # |j                  «        w xY w)NÚTRUEzRenriched_hash IS NULL OR enriched_hash IS DISTINCT FROM COALESCE(content_hash, %s)© )Úcursor_factoryzœSELECT product_key, name, description, brand, taxonomy_path, attributes, price_cents, price_reference_cents, content_hash FROM {}.strategist_products WHERE )ÚNO_CONTENT_HASHr	   Úcursorr   Úexecuter   ÚSQLÚformatÚ
IdentifierÚfetchallÚdictÚclose)r   r   ÚwhereÚparamsÚconnÚcurÚrs          r   Úselect_productsr/   &   sÖ   € Ù‰VðLð 
ñ ‰RœÐ0€FÜÓ€Dð	Ø�[‰[¬ˆ[Ó7ð 	5¸3Ø�K‰KœŸ™ð5à7<ñ=ó÷ ‰f”S—^‘^ IÓ.Ó/°ô	9ð
 &)§\¡\£^Ö4 ”D˜•GÒ4÷	5ð 	5ð 	�
‰
�ùò 5÷	5ð 	5úð 	5ð 	�
‰
�øˆ�
‰
�ús5   �C! ³ACÂB>Â"CÂ$	C! Â>CÃCÃC! Ã!C3c                 ó~  — t        «       }	 |j                  «       5 }|j                  t        j                  d«      j                  t        j                  | «      «      «       |j                  «       cddd«       |j                  «        S # 1 sw Y   nxY w	 |j                  «        y# |j                  «        w xY w)z:(total products, products holding at least one attribute).zgSELECT count(*), count(*) FILTER (WHERE jsonb_array_length(attributes) > 0) FROM {}.strategist_productsN)	r	   r"   r#   r   r$   r%   r&   Úfetchoner)   )r   r,   r-   s      r   Úcatalog_statsr2   8   s•   € äÓ€Dð	Ø�[‰[‹]ð 	"˜cØ�K‰KœŸ™ð.ó÷ ‰f”S—^‘^ IÓ.Ó/ô	1ð
 —<‘<“>÷	"ð 	"ð 	�
‰
�÷	"ð 	"úð 	"ð 	�
‰
�øˆ�
‰
�ús#   ŒB* œABÁ2	B* ÂBÂB* Â*B<Úproduct_keyÚ
attributesc           	      óö  — t        «       }	 |j                  «       5 }|j                  t        j                  d«      j                  t        j                  | «      «      t        |«      |||xs t        |f«       d d d «       |j                  «        	 |j                  «        y # 1 sw Y   Œ+xY w# t        $ r* |j                  «        t        j                  d|d¬«       ‚ w xY w# |j                  «        w xY w)Nz˜UPDATE {}.strategist_products SET attributes = %s, is_accessory = COALESCE(%s, is_accessory), price_tier = %s, enriched_hash = %s WHERE product_key = %sz#Could not persist attributes for %sT©Úexc_info)r	   r"   r#   r   r$   r%   r&   r   r!   ÚcommitÚ	ExceptionÚrollbackÚloggerÚerrorr)   )r   r3   r4   Úis_accessoryÚ
price_tierÚcontent_hashr,   r-   s           r   Úpersist_attributesr@   G   sØ   € äÓ€DðØ�[‰[‹]ð 	@˜cØ�K‰KœŸ™ðMó÷ ‰f”S—^‘^ IÓ.Ó/Ü�jÓ! <°ØÒ0¤°+ð?ô@÷	@ð 	�‰�ð 	�
‰
�÷	@ð 	@ûô ò Ø�‰ŒÜ�‰Ð:¸KØ"ð 	ô 	$àð	ûð 	�
‰
�ús/   ŒB0 œAB$Á:B0 Â$B-Â)B0 Â03C#Ã#C& Ã&C8c                 óÌ  — t        «       }	 |j                  «       5 }|j                  t        j                  d«      j                  t        j                  | «      «      |f«       ddd«       |j                  «        |j                  «        y# 1 sw Y   Œ*xY w# t        $ r+ |j                  «        t        j                  d|d¬«       Y ŒPw xY w# |j                  «        w xY w)z`Visible, not silent: a merchant should see a data gap rather than blame
    the recommendations.z¡UPDATE {}.strategist_products SET missing_fields = array_append(missing_fields, 'attributes') WHERE product_key = %s AND NOT ('attributes' = ANY(missing_fields))Nz"Could not flag %s as unextractableTr6   )r	   r"   r#   r   r$   r%   r&   r8   r9   r:   r;   r<   r)   )r   r3   r,   r-   s       r   Úflag_unextractablerB   ]   sÀ   € ô Ó€DðØ�[‰[‹]ð 	A˜cØ�K‰KœŸ™ðVó÷ ‰f”S—^‘^ IÓ.Ó/°+°ô	A÷	Að 	�‰Œð 	�
‰
�÷	Að 	Aûô ò $Ø�‰ŒÜ�‰Ð9¸;Ø"ð 	ö 	$ð$ûð
 	�
‰
�ús;   ŒB œA	BÁ%B ÂBÂB Â1CÃC ÃCÃC ÃC#ÚpercentÚstepc                 ó|   — | €y	  | t        |«      |«       y# t        $ r t        j                  dd¬«       Y yw xY w)ztProgress is best-effort. The job is spending real money on model calls;
    a reporting failure must never abort it.NzProgress callback failedTr6   )Úintr9   r;   Úwarning)ÚprogressrC   rD   s      r   Ú_reportrI   q   sC   € ð ÐØðBÙ”�W“˜tÕ$øÜò BÜ�‰Ð1¸DˆÖAðBús   … ˜ ;º;c                 ó  — t        | «       t        | |«      }t        | «      }|D �cg c]  }t        |«      sŒ|‘Œ }}|D �cg c]  }t        |«      rŒ|‘Œ }}|D ]  }t	        | |d   «       Œ dx}	x}
}t        |«      }|dk(  rt        |dd«       | t        z   }d}t        d|t        «      D �]m  }|||t        z    }|t        z  dz   }t        |«      }|s6|
t        |«      z  }
|t        |«      z  }t        ||dz  |z  d|› d|› �«       Œ_|D ]ã  }|j                  |d   «      }|s|
dz  }
Œ|j                  d«      xs g }t        |t        «      rt        j                  |«      }t        ||«      \  }}||z  }|j                  d	«      xs |j                  d
«      }t!        | |d   ||j                  d«      t#        ||«      xs |j                  d«      |j                  d«      «       |	dz  }	Œå |t        |«      z  }t        ||dz  |z  d|› d|› �«       �Œp t%        | «      \  }}|t        |«      |	|t        |«      z
  t        |«      |
||rt'        ||z  d«      nddœ}t(        j+                  d| |«       |S c c}w c c}w )Nr3   r   éd   u0   extracting attributes â€” no products to extracté   u    extracting attributes â€” batch z of r4   Úprice_reference_centsÚprice_centsr=   r>   r?   r   g        )ÚproductsÚ
consideredÚ	extractedÚskipped_unchangedÚskipped_unextractableÚfailedÚ	conflictsÚattribute_coveragezEnriched %s: %s)r
   r/   r   r   rB   r   rI   r   Úranger   r   Ú
isinstanceÚstrÚjsonÚloadsr   r@   r   r2   Úroundr;   Úinfo)r   r   rH   rO   ÚbandsÚpÚextractableÚskippedr   rQ   rT   rU   ÚtotalÚnum_batchesÚdoneÚstartÚbatchÚbatch_numberÚresultsÚinferredÚcurrentÚmergedÚproduct_conflictsÚpriceÚwith_attributesÚreports                             r   Úenrich_tenantrp   |   sÔ  € Ü˜9Ô%Ü˜y¨%Ó0€HÜ˜)Ó$€Eà&Ö<˜¬.¸Õ*;’1Ð<€KÐ<Ø"Ö<�Q¬.¸Õ*;ŠqÐ<€GÐ<Øò >ˆÜ˜9 g¨mÑ&<Õ=ð>ð &'Ð&€IÐ&�˜Ü�Ó€Eà�‚zÜ�˜#ÐQÔRà�FœjÑ(Ð)€KØ€Dä�q˜%¤Ó,ó &TˆØ˜E %¬*Ñ"4Ð5ˆØ¤
Ñ*¨QÑ.ˆÜ Ó&ˆÙØ”c˜%“jÑ ˆFØ”C˜“JÑˆDÜ�H˜d S™j¨EÑ1Ø6°|°nÀDÈÈÐVôXààò 	ˆGØ—{‘{ 7¨=Ñ#9Ó:ˆHÙØ˜!‘�Øà—k‘k ,Ó/Ò5°2ˆGÜ˜'¤3Ô'ÜŸ*™* WÓ-�ä(8¸À(Ó(KÑ%ˆFÐ%ØÐ*Ñ*ˆIà—K‘KÐ 7Ó8ÒV¸G¿K¹KÈÓ<VˆEô
 Ø˜7 =Ñ1°6Ø—‘˜^Ó,Ü˜ Ó&ÒD¨(¯,©,°|Ó*DØ—‘˜NÓ+ô	-ð
 ˜‰N‰Ið/	ð2 	”�E“
ÑˆÜ�˜$ ™*¨Ñ-Ø2°<°.ÀÀ[ÀMÐRö	TðK&TôP +¨9Ó5Ñ€Eˆ?ØÜ ›MØ$Ø#(¬3¨x«=Ñ#8Ü'*¨7£|ØØ$ÙIN¤E¨/¸EÑ*AÀ1Ô$EÐTWñY€Fô ‡K�KÐ! 9¨fÔ5Ø€MùòA =ùÚ<s   §J¸JÁJÁJ)FN)%Ú__doc__rZ   ÚloggingÚpsycopg2r   Úpsycopg2.extrasr   r   Ú!app.services.enrichment.extractorr   r   Úapp.services.enrichment.merger   Úapp.services.infra.databaser	   r
   Ú#app.services.enrichment.price_tiersr   r   Ú	getLoggerÚ__name__r;   r   r!   r(   Úboolr   rY   Úlistr/   Útupler2   r@   rB   rF   rI   rp   r   r   r   ú<module>r~      sú   ðñó Û å ß 0ç GÝ :ß Qß Gà	ˆ×	Ñ	˜8Ó	$€à€ð
 $€ð/˜Dð / Tó /ð˜sð ¨4ð °Dó ð$˜Sð  Uó ð #ð °Cð ÀTð ØBFóð, #ð °Cð ¸Dó ð(B˜sð B¨#ð B°$ó BñE˜Sð E¨ð EÈô Er   