ó
    ’j:j$  ã                  ó~  • S r SSKJ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	KJrJr   " S
 S\5      r " S S\5      r " S S\5      rS\R:                  " \R=                  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) uK  
TableUnifier Protocol â€” unifies the Camelot flavor outputs for a page
into one canonical table per physical table.

Camelot is run in two flavors (lattice + stream) on purpose: each
captures structure the other misses. When both produce an extraction of
the same physical table, the unifier combines them into one canonical
table (most complete header from one, full row set from the other),
using the page image as ground truth. Candidates that are genuinely
distinct tables pass through untouched. This is not deduplication â€” the
second flavor's output is a deliberate, complementary source, not an
unwanted duplicate.

For each page with multiple surviving classifier-positive candidates,
the unifier receives the page image and all candidates and returns the
canonical set. Pure-LLM (no geometric heuristics): the LLM judges which
candidates describe the same physical table and emits the unified best
version.

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_from)ÚDEFAULT_LLM_MODELÚget_settingsc                  ó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 unifier 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 unify decision)Údefaultr   z+Optional[Tuple[float, float, float, float]]Úbbox© )
Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__r   r   Ú__annotations__r   r   Ú__static_attributes__r   ó    Ú2/home/mande/repo/quber/src/quber/agents/unifier.pyr   r   &   s<   ‡ ÙÐ*lÑm€L�#ÓmÙÐ&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
)ÚUnifiedTableé0   z)Canonical markdown for this logical tabler   r   r   z„candidate_ids the unifier combined into this output. Single-id list means the candidate stood alone; multi-id means an actual unify.z	List[str]Úsource_candidate_idsr   N)	r   r    r!   r"   r   r   r#   r*   r$   r   r%   r&   r(   r(   0   s*   ‡ ÙÐ&QÑR€HˆcÓRÙ&+ðZñ'Ð˜)ö r%   r(   c                  ó.   • \ rS rSr% \" \SS9rS\S'   Srg)ÚUnifierResulté8   z?Canonical tables on this page after unifying the flavor outputs)Údefault_factoryr   zList[UnifiedTable]Útablesr   N)	r   r    r!   r"   r   Úlistr/   r#   r$   r   r%   r&   r,   r,   8   s   ‡ Ù!&ØØUñ"€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 unified 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)ÚTableUnifierég   c              ƒ  ó   #   • g 7f©Nr   )ÚselfÚ
page_imageÚ
candidatess      r&   ÚunifyÚTableUnifier.unifyi   s
   é € ð ùs   ‚r   N©r:   r   r;   úList[CandidateInput]Úreturnr,   )r   r    r!   r"   r<   r$   r   r%   r&   r5   r5   g   s#   † ðàðð )ðð 
÷	r%   r5   c                ór   • [         R                  " U  Vs/ s H  oR                  5       PM     snSS9$ s  snf )Nr1   r2   )ÚjsonÚdumpsÚ
model_dump)r;   Úcs     r&   Úcandidates_payloadrF   p   s/   € Ü�:Š:Ù!+Ó,¢˜A�‰Ž¡Ñ,Øñð ùÚ,s   •4c                  óZ   • \ rS rSrSr   S       S	S jjrS
S jr      SS jrSrg)ÚPydanticAIUnifieréw   uÆ   TableUnifier 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 unify
decisions.
Nc                ó¤  • SSK Jn  SSKJn  SSKJn  [        5       R                  nU=(       d    UR                  =(       d    [        nU=(       d    UR                  nU=(       d    UR                  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)Úmake_oauth_anthropic_model)Úapi_key)ÚproviderzqPydanticAIUnifier: neither ANTHROPIC_AUTH_TOKEN nor ANTHROPIC_API_KEY is set. Provide one via env or constructor.)Úoutput_typeÚsystem_promptzquber-unifier)Úrun_name)Úpydantic_airK   Úpydantic_ai.models.anthropicrL   Úpydantic_ai.providers.anthropicrM   r   ÚllmÚmodelr   Úanthropic_auth_tokenÚanthropic_api_keyÚquber.agents._oauth_gaterN   ÚRuntimeErrorr,   ÚUNIFY_FLAVORS_PROMPTÚagentr   Útracer)r9   rX   Ú
auth_tokenrO   rK   rL   rM   Úllm_settingsÚresolved_authÚresolved_keyrN   Ú
anth_modelrP   s                r&   Ú__init__ÚPydanticAIUnifier.__init__   s´   € õ 	&Ý?ÝEä#“~×)Ñ)ˆØ×@˜×+Ñ+×@Ô/@ˆØ"×G l×&GÑ&GˆØ×@ ,×"@Ñ"@ˆæÝKá3°EÓI‰JÞÙ(°Ñ>ˆHÙ'¨ÑA‰JäðPóð ð
 Œ
ÙØÜ%Ü.ñ
ˆŒ
ô &¨Ñ?ˆ�r%   c                ó2   • SS[         S.SSUS.SSS./S./0$ )	NÚmessagesÚsystem©ÚroleÚcontentÚuserÚtext)Útypern   Úimagez[page image attached])r]   )r9   Ú	user_texts     r&   Útrace_inputsÚPydanticAIUnifier.trace_inputs¥   s;   € àØ!Ô.BÑCà"à!'°Ñ;Ø!(Ð2IÑJð ñð	ð
ð 	
r%   c              ƒ  óX  #   • 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      S.Ul        S S S 5      IS h  v•N   U	$  N‰ Ng 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Úunify_flavors)rX   Ú	assistantrj   )rh   Útable_countÚusage_metadatazPunify: LLM call failed; returning candidates as-is page_image={} count={} exc={}©r   r*   )rT   ru   r,   rF   r   Ú
read_bytesrr   r_   Úllm_runrX   r^   ÚrunÚoutputÚmodel_dump_jsonÚlenr/   r   ÚusageÚoutputsÚ	Exceptionr   ÚerrorÚnamer(   r   r   )r9   r:   r;   ru   rq   rp   Úinputsr€   Úresultr�   ÚexcrE   s               r&   r<   ÚPydanticAIUnifier.unify³   sj  é € õ
 	.æÜ ¨Ñ+Ð+à0Ô1CÀJÓ1OÐ0PÐQˆ	Ù¤4¨
Ó#3×#>Ñ#>Ó#@È[ÑYˆØ×"Ñ" 9Ó-ˆð	Ø—{‘{×*Ñ*¨?¸FÏ*É*Ð*×UÑUÐY\Ø#Ÿz™zŸ~™~¨yÐ.@ÓA×A�ØŸ™�à*5À&×BXÑBXÓBZÑ![Ð \Ü#& v§}¡}Ó#5Ü&9¸&¿,¹,Ó&Gñ�”÷ V×Uð ˆMñ VÙA÷ V×U×Uð ˆMûÜó 	Ü�LŠLØbØ—‘Ü�J“Øô	ô !ñ (óâ'˜ô !¨!¯*©*ÈAÏNÉNÐK[Ô\Ú'ùôñõ ûð	üs©   ‚AF*Á(D* Á?D	Â D* Â DÂ#DÂ$ADÃ7D* ÄDÄD* ÄF*Ä	D* ÄDÄD* ÄD'ÄDÄD'Ä"D* Ä&F*Ä'D* Ä*
F'Ä45F"Å))F
Æ
F"ÆF'ÆF*Æ"F'Æ'F*)r^   rX   r_   )NNN)rX   úOptional[str]r`   r�   rO   r�   r@   ÚNone)rq   r   r@   zDict[str, Any]r>   )	r   r    r!   r"   Ú__doc__re   rr   r<   r$   r   r%   r&   rH   rH   w   si   † ñð  $Ø$(Ø!%ð	$@àð$@ð "ð$@ð ð	$@ð
 
õ$@ôL
ð#àð#ð )ð#ð 
÷	#r%   rH   c                  ó<   • \ rS rSrSrSSS jjr      S	S jrSrg)
ÚMockUnifieréÙ   zÄReturns a canned UnifierResult, or pass-through if none injected.

Pass-through behavior: emit one UnifiedTable per candidate, each
referencing only its own candidate_id (no unifying). For tests.
Nc                ó   • Xl         g r8   ©rŠ   )r9   rŠ   s     r&   re   ÚMockUnifier.__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}   rv   )rŠ   r,   r(   r   r   )r9   r:   r;   Ú_rE   s        r&   r<   ÚMockUnifier.unifyã   s]   é € ð
 ˆØ�;‰;Ñ"Ø—;‘;ÐÜáblóÚblÐ]^” a§j¡jÈÏÉÐGWÔXÑblññ
ð 	
ùòùs   ‚%A§)AÁ
Ar”   r8   )rŠ   zOptional[UnifierResult]r@   rŽ   r>   )r   r    r!   r"   r�   re   r<   r$   r   r%   r&   r‘   r‘   Ù   s-   † ñöð
àð
ð )ð
ð 
÷	
r%   r‘   )ÚapiÚmockc                ó°   • U =(       d    [        5       R                  R                  nUS:X  a
  [        5       $ US:X  a
  [	        5       $ [        SU< S35      e)Nr™   rš   zUnknown QUBER_UNIFIER_BACKEND: z. Expected api|mock.)r   rW   Úunifier_backendrH   r‘   Ú
ValueError)ÚbackendÚselecteds     r&   Úget_unifierr    õ   sQ   € Ø×<œ,›.×,Ñ,×<Ñ<€HØ�5ÓÜ Ó"Ð"Ø�6ÓÜ‹}ÐÜ
Ð6°x±lÐBVÐWÓ
XÐXr%   )r;   r?   r@   r   r8   )rž   zOptional[UnifierBackend]r@   r5   )&r�   Ú
__future__r   rB   Úpathlibr   Útypingr   r   r   r   r	   r
   r   r   Úlogurur   Úpydanticr   r   Úquber.agents.langsmith_tracerr   r   Úquber.settingsr   r   r   r(   r,   rC   Úmodel_json_schemar]   r5   rF   rH   r‘   ÚUnifierBackendr    r   r%   r&   Ú<module>rª      sÆ   ðñõ0 #ã Ý ß Y× YÓ Yå ß %ç Nß :ô�Yô ô�9ô ô�Iô ðð> ‡‚ˆM×+Ñ+Ó-°aÑ8Ð 9ð :ð?"Ð ðJ ô�8ó ó ðô÷_ñ _÷D
ñ 
ð2 ˜Ñ'€÷Yr%   