Ë
    §‘ˆj  ã                   óØ  — d Z ddlZddlZddlmZ ddlmZ ddlmZ ddl	m
Z
mZ  ej                  e«      ZdZdZi d	eef“d
ef“def“def“def“def“deef“def“deeef“deef“deef“def“def“def“def“def“Zej-                  «       D � �ch c]  \  } }e|v se|v r| ’Œ c}} Zd&dededefd„Zdededeeef   fd„Zd edefd!„Zdedefd"„Zd#ed$ededz  fd%„Zyc c}} w )'a’  Proposes a field map for an unknown product API from one sample record.

The proposal is never trusted: every path is executed against the sample before
it is returned, so a merchant is never shown a mapping that would fail at sync
time. Nothing is stored here -- the caller approves a map before it is saved.

This runs once, on a source's first sync, and its result is cached in
product_sources.config.fields. Every sync after that executes the stored map
directly with zero model calls -- if inference ran per product, the mapping
could drift between products, content_hash would drift with it, and the whole
catalog would re-embed nightly for no reason.
é    N)Úsettings)Úclient)ÚFIELD_MAP_PROPOSAL_PROMPT)ÚFieldMapErrorÚresolve_path)Úexternal_idÚtitleÚdescriptionÚbrandÚraw_categoryÚ	image_urlÚpriceÚdiscount_percentageÚavailabilityÚskuÚtagsÚrelated_idsÚcross_sellsÚupsellsÚratingÚreview_count)Úurl_templateÚcurrencyr   r	   r
   r   r   r   r   r   r   r   r   r   r   r   r   r   ÚrecordÚmax_value_lenÚreturnc                 ó  — i }| xs i j                  «       D ]l  \  }}|}t        |t        «      r|d| }n4t        |t        «      r|dd }nt        |t        «      rt        |«      dd }t        |«      j                  |dœ||<   Œn |S )zæReduce a record to shape -- keys, types, one truncated example each.

    A real product record can be tens of kilobytes of description prose; that
    teaches the model nothing about structure and costs money on every token.
    Né   é   )ÚtypeÚexample)ÚitemsÚ
isinstanceÚstrÚlistÚdictr    Ú__name__)r   r   ÚsummaryÚkeyÚvaluer!   s         úE/var/www/html/strategist-ai/app/services/catalog/fieldmap_proposer.pyÚsummarise_recordr,   B   s“   € ð €GØ’| ×*Ñ*Ó,ò J‰
ˆˆUØˆÜ�eœSÔ!Ø˜N˜]Ð+‰GÜ˜œtÔ$Ø˜B˜Q�i‰GÜ˜œtÔ$Ü˜5“k " 1�oˆGÜ $ U£× 4Ñ 4ÀÑIˆ�ŠðJð €Nó    ÚproposalÚsamplec           
      ó  — i g }}| xs i j                  «       D ]Â  \  }}|t        vr|j                  |«       Œ 	 t        ||«      }|€|j                  |«       ŒAt
        j                  |«      }|�f|t        v xr t        |t        «      }|st        ||«      s>t        j                  d|||t        |«      j                  «       |j                  |«       Œ¾|||<   ŒÄ |t        |«      fS # t        $ r |j                  |«       Y Œîw xY w)aç  Execute every proposed path against the sample; keep only what resolves
    AND fits the target's storage.

    A path resolving is not enough: on one real API "$.reviews" resolved
    cleanly to a list of review objects that was proposed for review_count,
    an INT column, and psycopg2 could not adapt it. Nothing the model says is
    trusted -- an unknown target name, a malformed path, a path that finds
    nothing, or a value of the wrong type is dropped rather than stored.
    z0Dropping field map %r -> %r: expected %s, got %s)r"   ÚTARGET_FIELDSÚappendr   r   ÚFIELD_TYPESÚgetÚ_NUMERIC_TARGETSr#   ÚboolÚloggerÚwarningr    r'   Úsorted)	r.   r/   ÚkeptÚdroppedÚtargetÚpathr*   ÚexpectedÚis_bad_bools	            r+   Úvalidate_proposalr@   U   s  € ð ˜ˆ'€DØ!š R×.Ñ.Ó0ò ‰ˆ�ØœÑ&Ø�N‰N˜6Ô"Øð	Ü  ¨Ó.ˆEð ˆ=Ø�N‰N˜6Ô"Øä—?‘? 6Ó*ˆØÐØ Ô$4Ð4ÒP¼ÀEÌ4Ó9PˆKÙ¤*¨U°HÔ"=Ü—‘ØFØ˜D (¬D°«K×,@Ñ,@ôð —‘˜vÔ&ØàˆˆVŠð1ð2 ”˜“Ð Ð øô' ò 	Ø�N‰N˜6Ô"Ùð	ús   ºC+Ã+DÄDr(   c           
      óv  — t         j                  j                  j                  t        j
                  ddidt        j                  dj                  t        «      ¬«      dœdt        j                  | t        ¬«      dœg¬	«      }t        j                  |j                  d
   j                  j                   «      S )Nr    Újson_objectÚsystemz, )Útargets)ÚroleÚcontentÚuser)Údefault)ÚmodelÚresponse_formatÚmessagesr   )r   ÚchatÚcompletionsÚcreater   Ú	LLM_MODELr   ÚformatÚjoinr1   ÚjsonÚdumpsr$   ÚloadsÚchoicesÚmessagerF   )r(   Úresponses     r+   Ú	_call_llmrX   |   s–   € Ü�{‰{×&Ñ&×-Ñ-Ü× Ñ Ø Ð/àÜ1×8Ñ8ØŸ™¤=Ó1ô3ñ4ð ¬¯
©
°7ÄCÔ(HÑIð	
ð .ó 	€Hô �:‰:�h×&Ñ& qÑ)×1Ñ1×9Ñ9Ó:Ð:r-   c                 óò   — 	 t        t        | «      «      }t        |t        «      st        j                  d«       i }t        || «      \  }}||dœS # t        $ r t        j	                  dd¬«       i }Y Œ_w xY w)zîAsk the model to map one sample record, then verify every answer.

    A model outage or a malformed response must leave the source connected and
    mappable by hand -- never break the sync, and never store an unvalidated
    guess.
    zField-map proposal failedT)Úexc_infoz.Field-map proposal was not an object; ignoring)Úfieldsr;   )	rX   r,   Ú	Exceptionr7   Úerrorr#   r&   r8   r@   )r/   Úrawr:   r;   s       r+   Úpropose_field_mapr_   Š   sw   € ðÜÔ(¨Ó0Ó1ˆô
 �cœ4Ô Ü�‰ÐGÔHØˆä% c¨6Ó2�M€Dˆ'Ø wÑ/Ð/øô ò Ü�‰Ð0¸4ˆÔ@ØŠðús   ‚A Á"A6Á5A6Úbase_urlr[   c                 óh   — | rd|xs i vry| j                  d«      d   j                  d«      }|› d�S )a�  Derive a per-product URL from a collection endpoint.

    A REST collection almost always addresses one member by id, so
    ".../products" plus an id mapping yields ".../products/{external_id}".
    Without an id mapping there is nothing to substitute, and guessing would
    give every product a broken link -- so this returns None and those
    products are stored with a null URL instead.
    r   Nú?r   ú/z/{external_id})ÚsplitÚrstrip)r`   r[   Úroots      r+   Úinfer_url_templaterg   Ÿ   sA   € ñ �}¨Vª\°rÑ:ØØ�>‰>˜#Ó˜qÑ!×(Ñ(¨Ó-€DØˆVÐ#Ð$Ð$r-   )éP   )Ú__doc__rR   ÚloggingÚapp.core.configr   Úapp.core.llm_clientr   Úapp.core.promptsr   Úapp.services.catalog.fieldmapr   r   Ú	getLoggerr'   r7   r1   ÚNOT_MODELLEDr$   ÚintÚfloatr%   r3   r"   r5   r&   r,   Útupler@   rX   r_   rg   )r<   Útypess   00r+   ú<module>ru      së  ðñó Û å $Ý &Ý 6ß Eà	ˆ×	Ñ	˜8Ó	$€ð€ð ,€ð
Ø˜C ˜:ðà˜C˜6ðð ˜C˜6ðð ˜C˜6ð	ð
 ˜C˜6ðð ˜C˜6ðð 
˜C ˜:ðð ˜C˜6ðð ˜C ¨Ð,ðð ˜C ˜<ðð ˜C ˜<ðð ˜C˜6ðð ˜D˜7ðð ˜D˜7ðð ˜D˜7ðð  ˜D˜7ð!€ð, !,× 1Ñ 1Ó 3÷Ùˆv�uØ
ˆe�|�u ‘~ò óÐ ñ˜Tð °#ð ¸tó ð&$! ð $!¨dð $!°u¸TÀ4¸ZÑ7Hó $!ðN;�tð ; ó ;ð0˜dð 0 tó 0ð*% ð %¨dð %°s¸T±zô %ùóGs   ÂC&