ó
    À‘´jy%  ã                  ó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  SS	KJrJr  SS
KJrJ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5      r" " S S5      r#\
S   r$SSS jjr%g)u{  
TableDetector Protocol â€” the page image is the arbiter of what is a
table and where.

The detector looks at a rendered page image and returns the real tables
on that page, in top-to-bottom reading order, each marked with a rough
bounding box and a short description. Its list is the reference point
for the whole Camelot-correspondence flow: it sets *how many* tables a
page has and *roughly where* each one sits. Camelot chunks are matched
to these detected tables downstream, in
`quber.core.extractors.camelot.correspondence` (`matching.assign_chunks`,
called from `orchestrator.py`).

The boxes should tightly bound each table: correspondence matches Camelot
chunks to detected tables by 2D bbox overlap, so box accuracy is
load-bearing. The description plays no part in matching. It becomes the
table's title when structure correction supplies none, and it is recorded
as `detected_description`.

Kept separate from `LLMClient`/`TableClassifier`/`TableUnifier` for the
same single-responsibility reasons: its own system prompt, its own
output schema, its own retry handling, and a model that can be tuned
independently of the other agents.
é    )ÚannotationsN)ÚPath)ÚAnyÚDictÚListÚLiteralÚOptionalÚProtocolÚTupleÚruntime_checkable)Úlogger)Ú	BaseModelÚField)Úsupports_sampling_temperature)ÚLangSmithTracerÚusage_metadata_from)ÚDEFAULT_LLM_MODELÚget_settingsc                  ó^   • \ rS rSr% \" SSS9rS\S'   \" SSS	9rS
\S'   \" SSS	9rS\S'   Sr	g)ÚDetectedTableé(   é   zI1-based position of this table in top-to-bottom reading order on the page)ÚgeÚdescriptionÚintÚordinalNzËBox tightly enclosing the table, normalized to 0..1 with the page's top-left as origin: (x1, y1, x2, y2). Correspondence matches Camelot chunks to detected tables by 2D bbox overlap, so accuracy matters.)Údefaultr   z+Optional[Tuple[float, float, float, float]]ÚbboxÚ u¾   Short description of what this table is â€” its visible title/caption or, if none, a one-line summary of its columns. The content hook the correspondence step matches Camelot chunks against.Ústrr   © )
Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__r   r   Ú__annotations__r   r   Ú__static_attributes__r!   ó    Ú3/home/mande/repo/quber/src/quber/agents/detector.pyr   r   (   sR   ‡ ÙØØ_ñ€GˆSó ñ 9>ØðQñ9€DÐ
5ó ñ ØðBñ€K�ö r(   r   c                  ó.   • \ rS rSr% \" \SS9rS\S'   Srg)ÚDetectorResulté?   zKReal tables on the page, ordered top to bottom. Empty if the page has none.)Údefault_factoryr   zList[DetectedTable]Útablesr!   N)	r"   r#   r$   r%   r   Úlistr.   r&   r'   r!   r(   r)   r+   r+   ?   s   ‡ Ù"'ØØañ#€FÐö r(   r+   u"  You are looking at a rendered image of a single PDF page. Identify the
*real data tables* on the page.

A real data table is a rectangular grid: a header naming columns AND at
least one row of aligned data cells. Report a table ONLY if you can see
both.

NEVER report these as tables:
- Charts or graphs (bar, line, pie) â€” even when they carry numeric data
  labels. A chart is not a table.
- Page furniture: footers, headers, decorative banners, logos, page
  numbers, section dividers.
- Footnote lists, narrative paragraphs, bullet lists, multi-column prose,
  Table-of-Contents entries.

Return every real table on the page, in top-to-bottom reading order:

- Number them with `ordinal` starting at 1 for the topmost table.
- Give each a `bbox` normalized to 0..1 (page top-left is the origin):
  (x1, y1, x2, y2). The box must TIGHTLY enclose the table's header and
  data cells â€” not the surrounding whitespace, caption, or footer. If you
  are not sure a table is present, do not report it.
- Give each a short `description`: its visible title/caption if it has
  one, otherwise a one-line summary of its columns. This is what later
  steps use to recognize the table, so make it specific.

If the page has no real tables, return an empty list.

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)ÚTableDetectorén   c              ƒ  ó   #   • g 7f©Nr!   )ÚselfÚ
page_images     r)   ÚdetectÚTableDetector.detectp   s   é € Àùs   ‚r!   N©r8   r   Úreturnr+   )r"   r#   r$   r%   r9   r'   r!   r(   r)   r3   r3   n   s   † çCr(   r3   c                  óX   • \ rS rSrSrSrSSS\4         S	S jjrS
S jrSS jrSr	g)ÚPydanticAITableDetectorés   uÝ  TableDetector backed by pydantic-ai with an Anthropic model.

Sends the page image as `BinaryContent`; the image is the arbiter of
what tables exist. Auth resolution mirrors the other agents:
`ANTHROPIC_AUTH_TOKEN` (OAuth subscription) preferred over
`ANTHROPIC_API_KEY`.

Model defaults to Haiku 4.5 â€” the codebase default â€” but the detector
is the agent most likely to move to a stronger model, since the table
count it returns is load-bearing for everything downstream.
g        Nc                óþ  • SSK Jn  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b  [        U5      (       d  S 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
        X@l        Uc  S OU" US
9nU" U[&        [(        US9U l        [-        SS9U l        g )Nr   )ÚAgent)ÚAnthropicModel)ÚAnthropicProvider)ÚModelSettings)Úmake_oauth_anthropic_model)Úapi_key)ÚproviderzwPydanticAITableDetector: neither ANTHROPIC_AUTH_TOKEN nor ANTHROPIC_API_KEY is set. Provide one via env or constructor.)Útemperature)Úoutput_typeÚsystem_promptÚmodel_settingszquber-detector)Úrun_name)Úpydantic_airA   Úpydantic_ai.models.anthropicrB   Úpydantic_ai.providers.anthropicrC   Úpydantic_ai.settingsrD   r   ÚllmÚmodelr   r   Úanthropic_auth_tokenÚanthropic_api_keyÚquber.agents._oauth_gaterE   ÚRuntimeErrorrH   r+   ÚDETECT_TABLES_PROMPTÚagentr   Útracer)r7   rR   Ú
auth_tokenrF   rH   rA   rB   rC   rD   Úllm_settingsÚresolved_authÚresolved_keyrE   Ú
anth_modelrG   rK   s                   r)   Ú__init__Ú PydanticAITableDetector.__init__ˆ   sé   € õ 	&Ý?ÝEÝ6ä#“~×)Ñ)ˆØ×@˜×+Ñ+×@Ô/@ˆØÑ"Ô+HÈ×+OÑ+OØˆKØ"×G l×&GÑ&GˆØ×@ ,×"@Ñ"@ˆæÝKá3°EÓI‰JÞÙ(°Ñ>ˆHÙ'¨ÑA‰JäðPóð ð
 Œ
Ø&ÔØ!,Ñ!4™¹-ÐT_Ñ:`ˆÙØÜ&Ü.Ø)ñ	
ˆŒ
ô &Ð/?Ñ@ˆ�r(   c                ó2   • SS[         S.SSSS.SS	S./S./0$ )
NÚmessagesÚsystem©ÚroleÚcontentÚuserÚtextúDetect the tables on this page.)Útyperh   Úimagez[page image attached])rW   )r7   s    r)   Útrace_inputsÚ$PydanticAITableDetector.trace_inputsµ   s<   € àØ!Ô.BÑCà"à!'Ð1RÑSØ!(Ð2IÑJð ñð	ð
ð 	
r(   c              ƒ  ó²  #   • SSK Jn  U" [        U5      R                  5       SS9nU R	                  5       n U R
                  R                  SX@R                  S9 IS h  v•N nU R                  R                  SU/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! ["         a>  n[$        R&                  " S[        U5      R(                  U5        [+        / S9s S nA$ S nAff = f7f)Nr   )ÚBinaryContentz	image/png)ÚdataÚ
media_typeÚdetect_tables)rR   ri   Ú	assistantrd   )rb   Útable_countÚusage_metadatazGdetect: LLM call failed; returning empty detection page_image={} exc={}©r.   )rM   ro   r   Ú
read_bytesrl   rY   Úllm_runrR   rX   ÚrunÚoutputÚmodel_dump_jsonÚlenr.   r   ÚusageÚoutputsÚ	Exceptionr   ÚerrorÚnamer+   )	r7   r8   ro   rk   Úinputsry   Úresultrz   Úexcs	            r)   r9   ÚPydanticAITableDetector.detectÃ   s  é € Ý-á¤4¨
Ó#3×#>Ñ#>Ó#@È[ÑYˆØ×"Ñ"Ó$ˆð	-Ø—{‘{×*Ñ*¨?¸FÏ*É*Ð*×UÑUÐY\Ø#Ÿz™zŸ~™~Ð/PÐRWÐ.XÓY×Y�ØŸ™�à*5À&×BXÑBXÓBZÑ![Ð \Ü#& v§}¡}Ó#5Ü&9¸&¿,¹,Ó&Gñ�”÷ V×Uð ˆMñ VÙY÷ V×U×Uð ˆMûÜó 	-Ü�LŠLØYÜ�ZÓ ×%Ñ%Øôô
 "¨Ñ,Õ,ûð	-üs›   ‚5E¸(D Á C+Á!D Á$!C1ÂC-ÂAC1ÃD Ã$C/Ã%D Ã*EÃ+D Ã-C1Ã/D Ã1D	Ã7C:Ã8D	ÄD ÄEÄ	D Ä
EÄ3EÅ	EÅ
EÅEÅE)rX   rR   rH   rY   )
rR   úOptional[str]rZ   r†   rF   r†   rH   zOptional[float]r<   ÚNone)r<   zDict[str, Any]r;   )
r"   r#   r$   r%   Ú__doc__ÚDEFAULT_TEMPERATUREr_   rl   r9   r'   r!   r(   r)   r>   r>   s   sc   † ñ
ð$ Ðð  $Ø$(Ø!%Ø':ð+Aàð+Að "ð+Að ð	+Að
 %ð+Að 
õ+AôZ
÷-r(   r>   c                  ó0   • \ rS rSrSrSSS jjrS	S jrSrg)
ÚMockTableDetectoréÛ   z+Returns a canned DetectorResult. For tests.Nc                ó2   • U=(       d	    [        / S9U l        g )Nrv   )r+   rƒ   )r7   rƒ   s     r)   r_   ÚMockTableDetector.__init__Þ   s   € Ø×9¤°bÑ 9ˆ�r(   c              ƒ  ó&   #   • UnU R                   $ 7fr6   ©rƒ   )r7   r8   Ú_s      r)   r9   ÚMockTableDetector.detectá   s   é € ØˆØ�{‰{Ðùs   ‚r�   r6   )rƒ   zOptional[DetectorResult]r<   r‡   r;   )r"   r#   r$   r%   rˆ   r_   r9   r'   r!   r(   r)   r‹   r‹   Û   s   † Ù5ö:÷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_DETECTOR_BACKEND: z. Expected api|mock.)r   rQ   Údetector_backendr>   r‹   Ú
ValueError)ÚbackendÚselecteds     r)   Úget_detectorrš   é   sR   € Ø×=œ,›.×,Ñ,×=Ñ=€HØ�5ÓÜ&Ó(Ð(Ø�6ÓÜ Ó"Ð"Ü
Ð7¸±|ÐCWÐXÓ
YÐYr(   r6   )r˜   zOptional[DetectorBackend]r<   r3   )&rˆ   Ú
__future__r   ÚjsonÚpathlibr   Útypingr   r   r   r   r	   r
   r   r   Úlogurur   Úpydanticr   r   Úquber.agents.factoryr   Úquber.agents.langsmith_tracerr   r   Úquber.settingsr   r   r   r+   ÚdumpsÚmodel_json_schemarW   r3   r>   r‹   ÚDetectorBackendrš   r!   r(   r)   Ú<module>r§      s»   ðñõ2 #ã Ý ß Y× YÓ Yå ß %å >ß Nß :ô�Iô ô.�Yô ðð> ‡‚ˆN×,Ñ,Ó.°qÑ9Ð :ð ;ð?"Ð ðJ ôD�Hó Dó ðD÷e-ñ e-÷Pñ ð ˜-Ñ(€÷Zr(   