ó
    Rƒ'j"  ã                  óv  • S r SSKJr  SSKrSSKrSSKJr  SSKJrJ	r	J
r
JrJrJrJrJr  SSKJr  SSKJrJr  SSKJrJr   " S	 S
\5      r " S S\5      r " S S\5      rS\R6                  " \R9                  5       SS9 S3r\ " S S\5      5       rSS jr " S S5      r  " S S5      r!\S   r"SSS jjr#g)ul  
TableMerger Protocol â€” collapses duplicate candidates produced by
running Camelot in multiple flavors.

For each page that has multiple surviving classifier-positive
candidates, the merger receives the page image and all candidates,
and returns the canonical merged set. Pure-LLM dedup (no geometric
heuristics) â€” the LLM judges what is the same logical table and
produces the merged best version, using the page image as ground
truth.

Kept separate from `LLMClient` for the same reasons as
`TableClassifier`: single-responsibility, potentially different model
than structure-correction, isolated retry semantics.
é    )ÚannotationsN)ÚPath)ÚAnyÚDictÚListÚLiteralÚOptionalÚProtocolÚTupleÚruntime_checkable)Úlogger)Ú	BaseModelÚField)ÚLangSmithTracerÚusage_metadata_fromc                  óZ   • \ rS rSr% \" SS9rS\S'   \" SS9rS\S'   \" SS	S
9rS\S'   Sr	g)ÚCandidateInputé   z?Stable id used to reference this candidate in the merger output©ÚdescriptionÚstrÚcandidate_idz-Camelot-extracted markdown for this candidateÚmarkdownNzfCamelot's geometric box (x1, y1, x2, y2); informational, not load-bearing for the LLM's merge decision)Údefaultr   z+Optional[Tuple[float, float, float, float]]Úbbox© )
Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__r   r   Ú__annotations__r   r   Ú__static_attributes__r   ó    Ú1/home/mande/repo/quber/src/quber/agents/merger.pyr   r      s<   ‡ ÙÐ*kÑl€L�#ÓlÙÐ&UÑV€HˆcÓVÙ8=Øð8ñ9€DÐ
5ö r#   r   c                  óB   • \ rS rSr% \" SS9rS\S'   \" SS9rS\S'   S	rg
)ÚMergedTableé(   z)Canonical markdown for this logical tabler   r   r   z„candidate_ids the merger collapsed into this output. Single-id list means the candidate stood alone; multi-id means an actual merge.z	List[str]Úsource_candidate_idsr   N)	r   r   r   r    r   r   r!   r(   r"   r   r#   r$   r&   r&   (   s*   ‡ ÙÐ&QÑR€HˆcÓRÙ&+ðZñ'Ð˜)ö r#   r&   c                  ó.   • \ rS rSr% \" \SS9rS\S'   Srg)ÚMergerResulté0   z/Canonical tables on this page after dedup-merge)Údefault_factoryr   zList[MergedTable]Útablesr   N)	r   r   r   r    r   Úlistr-   r!   r"   r   r#   r$   r*   r*   0   s   ‡ Ù %ØØEñ!€FÐö r#   r*   u*  You are reconciling candidate tables extracted from a single PDF page
by different table-extraction strategies. Multiple candidates may
represent the same logical table (extracted twice with slight
differences) or distinct tables that happen to be on the same page.

You are given:

1. A rendered image of the PDF page (ground truth).
2. A list of candidate tables, each with an id, markdown, and optional bbox.

Your job: return the canonical set of tables present on this page.

Rules:

- *Same logical table* â€” two candidates describe the same physical
  table on the page (even if their bboxes, row counts, or cell
  content differ slightly). Return ONE merged entry combining the
  best parts of each (most complete header, all data rows, correct
  column count). List ALL contributing candidate_ids in
  `source_candidate_ids`.
- *Distinct tables* â€” two candidates are different physical tables
  that happen to be on the same page. Return each as its own entry
  with `source_candidate_ids` containing only its own id.
- *Use the page image* as the source of truth for which physical
  tables exist and what their structure should be.
- *Preserve numeric values exactly* from whichever candidate(s) you
  draw them from. Do not invent or alter numbers.

Return a JSON object conforming exactly to this schema:

é   ©ÚindentuE   

Return only the JSON object â€” no prose, no markdown code fences.
c                  ó*   • \ rS rSr      SS jrSrg)ÚTableMergeré_   c              ƒ  ó   #   • g 7f©Nr   )ÚselfÚ
page_imageÚ
candidatess      r$   ÚmergeÚTableMerger.mergea   s
   é € ð ùs   ‚r   N©r8   r   r9   úList[CandidateInput]Úreturnr*   )r   r   r   r    r:   r"   r   r#   r$   r3   r3   _   s#   † ðàðð )ðð 
÷	r#   r3   c                ór   • [         R                  " U  Vs/ s H  oR                  5       PM     snSS9$ s  snf )Nr/   r0   )ÚjsonÚdumpsÚ
model_dump)r9   Úcs     r$   Úcandidates_payloadrD   h   s/   € Ü�:Š:Ù!+Ó,¢˜A�‰Ž¡Ñ,Øñð ùÚ,s   •4c                  ó^   • \ rS rSrSrSr   S	       S
S jjrSS jr      SS jrSr	g)ÚPydanticAIMergeréo   uÅ   TableMerger backed by pydantic-ai with an Anthropic model.

Sends the page image as `BinaryContent` alongside the candidate
payload â€” the image is the ground-truth reference for dedup
decisions.
zclaude-haiku-4-5-20251001Nc                óú  • SSK Jn  SSKJn  SSKJn  U=(       d2    [        R                  R                  S5      =(       d    U R                  nU=(       d    [        R                  R                  S5      nU=(       d    [        R                  R                  S5      nU(       a  SSK
Jn	  U	" X5      n
OU(       a  U" US	9nU" XS
9n
O[        S5      eXl        U" U
[        [        S9U l        [#        SS9U l        g )Nr   )ÚAgent)ÚAnthropicModel)ÚAnthropicProviderÚQUBER_LLM_MODELÚANTHROPIC_AUTH_TOKENÚANTHROPIC_API_KEY)Úmake_oauth_anthropic_model)Úapi_key)ÚproviderzpPydanticAIMerger: neither ANTHROPIC_AUTH_TOKEN nor ANTHROPIC_API_KEY is set. Provide one via env or constructor.)Úoutput_typeÚsystem_promptzquber-merger)Úrun_name)Úpydantic_airI   Úpydantic_ai.models.anthropicrJ   Úpydantic_ai.providers.anthropicrK   ÚosÚenvironÚgetÚDEFAULT_MODELÚquber.agents._oauth_gaterO   ÚRuntimeErrorÚmodelr*   ÚMERGE_CANDIDATES_PROMPTÚagentr   Útracer)r7   r^   Ú
auth_tokenrP   rI   rJ   rK   Úresolved_authÚresolved_keyrO   Ú
anth_modelrQ   s               r$   Ú__init__ÚPydanticAIMerger.__init__y   sÄ   € õ 	&Ý?ÝEà×PœŸ™Ÿ™Ð(9Ó:×P¸d×>PÑ>PˆØ"×L¤b§j¡j§n¡nÐ5KÓ&LˆØ×E¤"§*¡*§.¡.Ð1DÓ"EˆæÝKá3°EÓI‰JÞÙ(°Ñ>ˆHÙ'¨ÑA‰JäðPóð ð
 Œ
ÙØÜ$Ü1ñ
ˆŒ
ô &¨~Ñ>ˆ�r#   c                ó2   • SS[         S.SSUS.SSS./S./0$ )	NÚmessagesÚsystem©ÚroleÚcontentÚuserÚtext)Útypero   Úimagez[page image attached])r_   )r7   Ú	user_texts     r$   Útrace_inputsÚPydanticAIMerger.trace_inputsž   s;   € àØ!Ô.EÑFà"à!'°Ñ;Ø!(Ð2IÑJð ñð	ð
ð 	
r#   c              ƒ  ó`  #   • SSK Jn  U(       d	  [        / S9$ S[        U5       3nU" [	        U5      R                  5       SS9nU R                  U5      n U R                  R                  SX`R                  S9 IS h  v•N nU R                  R                  XE/5      I S h  v•N nUR                  n	S	U	R                  5       S
./[        U	R                  5      [!        UR#                  5       5      S.Ul        S S S 5      IS h  v•N   U	$  N� Nk N
! , IS h  v•N  (       d  f       W	$ = f! [&         as  n
[(        R*                  " SUR,                  [        U5      U
5        [        U Vs/ s H"  n[/        UR0                  UR2                  /S9PM$     Os  snf snS9s S n
A
$ S n
A
ff = f7f)Nr   )ÚBinaryContent©r-   zCandidates on this page:
z	image/png)ÚdataÚ
media_typeÚmerge_candidates)r^   Ú	assistantrk   )ri   Útable_countÚusage_metadatazPmerge: LLM call failed; returning candidates as-is page_image={} count={} exc={}©r   r(   )rU   rv   r*   rD   r   Ú
read_bytesrs   ra   Úllm_runr^   r`   ÚrunÚoutputÚmodel_dump_jsonÚlenr-   r   ÚusageÚoutputsÚ	Exceptionr   ÚerrorÚnamer&   r   r   )r7   r8   r9   rv   rr   rq   Úinputsr�   Úresultr‚   ÚexcrC   s               r$   r:   ÚPydanticAIMerger.merge¬   sm  é € õ
 	.æÜ rÑ*Ð*à0Ô1CÀJÓ1OÐ0PÐQˆ	Ù¤4¨
Ó#3×#>Ñ#>Ó#@È[ÑYˆØ×"Ñ" 9Ó-ˆð	Ø—{‘{×*Ñ*Ð+=¸vÏZÉZÐ*×XÑXÐ\_Ø#Ÿz™zŸ~™~¨yÐ.@ÓA×A�ØŸ™�à*5À&×BXÑBXÓBZÑ![Ð \Ü#& v§}¡}Ó#5Ü&9¸&¿,¹,».Ó&Iñ�”÷ Y×Xð ˆMñ YÙA÷ Y×X×Xð ˆMûÜó 	Ü�LŠLØbØ—‘Ü�J“Øô	ô  ñ (óâ'˜ô  ¨¯©È1Ï>É>ÐJZÔ[Ú'ùôñõ ûð	üs©   ‚AF.Á(D. Á?DÂ D. Â DÂ#DÂ$ADÃ;D. ÄDÄD. ÄF.ÄD. ÄDÄD. ÄD+ÄDÄD+Ä&D. Ä*F.Ä+D. Ä.
F+Ä85F&Å-)F
Æ
F&Æ F+Æ!F.Æ&F+Æ+F.)r`   r^   ra   )NNN)r^   úOptional[str]rb   rŽ   rP   rŽ   r>   ÚNone)rr   r   r>   zDict[str, Any]r<   )
r   r   r   r    Ú__doc__r[   rf   rs   r:   r"   r   r#   r$   rF   rF   o   sk   † ñð 0€Mð  $Ø$(Ø!%ð	#?àð#?ð "ð#?ð ð	#?ð
 
õ#?ôJ
ð#àð#ð )ð#ð 
÷	#r#   rF   c                  ó<   • \ rS rSrSrSSS jjr      S	S jrSrg)
Ú
MockMergeréÒ   zÁReturns a canned MergerResult, or pass-through if none injected.

Pass-through behavior: emit one MergedTable per candidate, each
referencing only its own candidate_id (no merging). For tests.
Nc                ó   • Xl         g r6   ©r‹   )r7   r‹   s     r$   rf   ÚMockMerger.__init__Ù   s   € Ø�r#   c           
   ƒ  ó¸   #   • UnU R                   b  U R                   $ [        U Vs/ s H"  n[        UR                  UR                  /S9PM$     snS9$ s  snf 7f)Nr~   rw   )r‹   r*   r&   r   r   )r7   r8   r9   Ú_rC   s        r$   r:   ÚMockMerger.mergeÜ   s]   é € ð
 ˆØ�;‰;Ñ"Ø—;‘;ÐÜáakóÚakÐ\]” Q§Z¡ZÀqÇ~Á~ÐFVÔWÑakññ
ð 	
ùòùs   ‚%A§)AÁ
Ar•   r6   )r‹   zOptional[MergerResult]r>   r�   r<   )r   r   r   r    r�   rf   r:   r"   r   r#   r$   r’   r’   Ò   s-   † ñöð
àð
ð )ð
ð 
÷	
r#   r’   )ÚapiÚmockc                ó´   • U =(       d     [         R                  R                  SS5      nUS:X  a
  [        5       $ US:X  a
  [	        5       $ [        SU< S35      e)NÚQUBER_MERGER_BACKENDrš   r›   zUnknown QUBER_MERGER_BACKEND: z. Expected api|mock.)rX   rY   rZ   rF   r’   Ú
ValueError)ÚbackendÚselecteds     r$   Ú
get_mergerr¡   î   sS   € Ø×Gœ"Ÿ*™*Ÿ.™.Ð)?ÀÓG€HØ�5ÓÜÓ!Ð!Ø�6ÓÜ‹|ÐÜ
Ð5°h±\ÐAUÐVÓ
WÐWr#   )r9   r=   r>   r   r6   )rŸ   zOptional[MergerBackend]r>   r3   )$r�   Ú
__future__r   r@   rX   Úpathlibr   Útypingr   r   r   r   r	   r
   r   r   Úlogurur   Úpydanticr   r   Úquber.agents.langsmith_tracerr   r   r   r&   r*   rA   Úmodel_json_schemar_   r3   rD   rF   r’   ÚMergerBackendr¡   r   r#   r$   Ú<module>rª      sÆ   ðñõ  #ã Û 	Ý ß Y× YÓ Yå ß %ç Nô�Yô ô�)ô ô�9ô ðð> ‡‚ˆL×*Ñ*Ó,°QÑ7Ð 8ð 9ð?"Ð ðJ ô�(ó ó ðô÷`ñ `÷F
ñ 
ð2 ˜Ñ&€÷Xr#   