
    `gjv                    b   d Z ddlmZ ddlZddlZddlZddlZddlZddlm	Z	 ddl
mZmZmZmZmZ  ej                   e      Z eh d      ZdZdj-                  d	 eD              Z ej0                  d
ez   dz   ej2                        Z ej0                  dez   dz   ej2                        Zd dZ eh d      Z eh d      Zd!dZ	 	 	 	 	 	 	 	 d"dZ 	 	 	 	 	 	 d#dZ!d$dZ"d%dZ#d&dZ$	 d'	 	 	 	 	 	 	 d(dZ%	 	 	 	 	 	 	 	 d)dZ&d*dZ' eh d      Z(d+dZ)d'd,dZ*d-dZ+	 d'	 	 	 	 	 	 	 d.dZ,g dZ-y)/u  Routing helpers for inbound user-attached images.

Two modes:

  native  — attach images as OpenAI-style ``image_url`` content parts on the
            user turn. Provider adapters (Anthropic, Gemini, Bedrock, Codex,
            OpenAI chat.completions) already translate these into their
            vendor-specific multimodal formats.

  text    — run ``vision_analyze`` on each image up-front and prepend the
            description to the user's text. The model never sees the pixels;
            it only sees a lossy text summary. This is the pre-existing
            behaviour and still the right choice for non-vision models.

The decision is made once per message turn by :func:`decide_image_input_mode`.
It reads ``agent.image_input_mode`` from config.yaml (``auto`` | ``native``
| ``text``, default ``auto``) and the active model's capability metadata.

In ``auto`` mode:
  - If the active model reports ``supports_vision=True`` (via config
    override or models.dev metadata), we attach natively — vision-capable
    main models should always see the original pixels, even when an
    auxiliary vision backend is configured. That auxiliary backend then
    acts as a *fallback* for sessions whose main model can't take images.
  - Otherwise, if the user has explicitly configured ``auxiliary.vision``
    (provider/model/base_url not ``auto``/empty), we route through the
    text pipeline so the auxiliary vision backend can describe the image
    for the text-only main model.
  - Otherwise (non-vision model, no explicit override), we fall back to
    text via the default vision_analyze flow.

This keeps ``vision_analyze`` surfaced as a tool in every session — skills
and agent flows that chain it (browser screenshots, deeper inspection of
URL-referenced images, style-gating loops) keep working. The routing only
affects *how user-attached images on the current turn* are presented to the
main model.
    )annotationsN)Path)AnyDictListOptionalTuple>   autotextnative)	.png.jpg.jpeg.gif.webp.bmpz.tiffz.tifz.heic|c              #  >   K   | ]  }|j                  d         yw).N)lstrip).0es     F/root/.hermes/venv/lib/python3.12/site-packages/agent/image_routing.py	<genexpr>r   ?   s     AahhsmAs   z/(?<![/:\w.])(?:~/|/)(?:[\w.\-]+/)*[\w.\-]+\.(?:z)\bzhttps?://[^\s<>\"']+?\.(?:z)(?:\?[^\s<>\"']*)?c                &   t        | t              r| sg g fS g t        j                  d| t        j                        D ]1  }j                  |j                         |j                         f       3 t        j                  d|       D ]1  }j                  |j                         |j                         f       3 dfd}g }t               }t        j                  |       D ]  } ||j                               r|j                  d      }t        j                  j                  |      }	 t        j                  j                  |      sk	 ||v rq|j!                  |       |j                  |        g }t               }	t"        j                  |       D ]b  } ||j                               r|j                  d      }
|
j%                  d      }
|
|	v rA|	j!                  |
       |j                  |
       d ||fS # t        $ r Y +w xY w)u  Scan free-form text for image references the model should see.

    Returns ``(local_paths, urls)``:

      * ``local_paths`` — absolute (``/``) or home-relative (``~/``) paths
        whose suffix is an image extension AND whose expanded form exists
        on disk as a file. Order-preserving, deduplicated.
      * ``urls`` — ``http(s)://…`` URLs whose path ends in an image
        extension (a ``?query`` is allowed after the extension).
        Order-preserving, deduplicated.

    Matches inside fenced code blocks (``` ``` ```) and inline backticks
    (`` `…` ``) are skipped so that snippets pasted into a task body for
    reference aren't mistaken for live attachments. This mirrors the
    behaviour of ``gateway.platforms.base.BaseAdapter.extract_local_files``.

    Local paths are validated against the filesystem; URLs are not
    (the provider fetches them at request time).
    z```[^\n]*\n.*?```z	`[^`\n]+`c                .     t         fdD              S )Nc              3  D   K   | ]  \  }}|cxk  xr |k  nc   y wN )r   sr   poss      r   r   z7extract_image_refs.<locals>._in_code.<locals>.<genexpr>r   s     7DAq1<a<<7s    )any)r!   
code_spanss   `r   _in_codez$extract_image_refs.<locals>._in_codeq   s    7J777    r   z	.,;:!?)]>)r!   intreturnbool)
isinstancestrrefinditerDOTALLappendstartendset_LOCAL_IMAGE_PATH_REgroupospath
expanduserisfileOSErroradd_IMAGE_URL_RErstrip)r   mr$   local_paths
seen_pathsmatchrawexpandedurls	seen_urlsurlr#   s              @r   extract_image_refsrE   R   s   ( dC 2v )+J[[-tRYY? 01779aeeg./0[[t, 01779aeeg./08  K5J%..t4 %EKKM"kk!n77%%c*	77>>(+ ,
 z!x 8$%  D%I''- 
EKKM"kk!n jj%)cC
 -  		s   /H	HH>   1onyestrue>   0noofffalsec                    t        | t              r| S t        | t              r| dv rt        |       S yt        | t              r0| j	                         j                         }|t        v ry|t        v ryy)z@Return True/False for recognised boolean values, None otherwise.)r      NTF)r)   r(   r&   r*   striplower_TRUE_TOKENS_FALSE_TOKENS)r@   r    s     r   _coerce_capability_boolrT      sf    #t
#s&=9#sIIKr%   c                   t        | t              sy| j                  d      }t        |t              r|ni }t        |j                  d            }||S t	        |j                  d      xs d      j                         }d}|j                  d      r|t        d      d }| j                  d      }t        |t              r|ni }	t        j                  t        d|||f            D ]  }
|	j                  |
      }t        |t              r|ni }|j                  d      }t        |t              r|ni }|j                  |      }t        |t              r|ni }t        |j                  d            }||c S  | j                  d	      }t        |t              r$t               }t        d||f      D ]V  }
|j                  |
       |
j                  d      r|j                  |
t        d      d        C|j                  d|
        X |D ]  }t        |t              st	        |j                  d
      xs d      j                         }||vrE|j                  d      }t        |t              r|ni }|j                  |      }t        |t              r|ni }t        |j                  d            }||c S  y)u  Resolve user-declared vision capability from config.yaml.

    Resolution order, first hit wins:
      1. ``model.supports_vision`` (top-level shortcut for the active model)
      2. ``providers.<provider>.models.<model>.supports_vision``
         (named custom providers — ``provider`` may be the runtime-resolved
         value ``"custom"`` and/or the user-declared name under
         ``model.provider``; both are tried. For ``custom:<name>`` syntax,
         the stripped ``<name>`` is also tried as a provider key.)

    Returns None when no override is set, so the caller falls through to
    models.dev. Returns False explicitly only when the user wrote a
    recognised boolean false token.
    Nmodelsupports_visionprovider custom:	providersmodelscustom_providersname)r)   dictgetrT   r*   rP   
startswithlenfromkeysfilterlistr1   r9   )cfgrX   rV   model_cfg_raw	model_cfgtopconfig_providerstripped_suffixproviders_rawproviders_cfgp	entry_rawentry
models_raw
models_cfgper_model_raw	per_modelcoercedr]   candidate_names
entry_names                        r   _supports_vision_overriderx      s   & c4  GGG$M1;M41PVXI
!)--0A"B
CC

 )--
39r:@@BOO!!),)#i./:GGK(M5?t5TMZ\M]]6$?O(TUV 	!%%a(	-7	4-H	bYYx(
3=j$3OZUW
"u-5?t5TMZ\	))--8I*JKN	 ww12"D)  #ux9: 	3A"||I&##Ac)no$67##gaSM2	3 * 	Ii.Y]]628b9??AJ0"x0J'1*d'CJ&NN51M)3M4)HbI-imm<M.NOG"	 r%   c                   	 ddl m} t         |d      xs d      j                         }t         |d      xs d      j                         j	                         }t        |xs d      j                         j	                         }|r	|r||k(  r|S t        | t              sy| j                  d      }t        |t              r|ni }t        |j                  d      xs d      j                         }|r|S t        |j                  d      xs d      j                         }	t               }
t        d||	f      D ]k  }|
j                  |       |j	                         j                  d      r%|
j                  |j                  d	d
      d
          X|
j                  d|        m | j                  d      }t        |t              r[|
D ]V  }|j                  |      }t        |t              s%t        |j                  d      xs d      j                         }|sT|c S  | j                  d      }t        |t              r|
D ch c]  }|j	                          }}|D ]  }t        |t              st        |j                  d      xs d      j                         }||
vr|j	                         |vrWt        |j                  d      xs d      j                         }|s|c S  y# t
        $ r Y lw xY wc c}w )z7Best-effort base URL for the active inference provider.r   )_runtime_main_valuebase_urlrY   rX   rV   NrZ   :rO   r[   r]   r^   )agent.auxiliary_clientrz   r*   rP   rQ   	Exceptionr)   r_   r`   r1   rd   r9   ra   splitre   )rf   rX   rz   runtimeruntime_providerrequested_providerrg   rh   r{   rj   rv   rn   rm   r^   rp   bur]   nloweredro   rw   s                        r   _resolve_inference_base_urlr     s   
	>)*5;<BBD2:>D"EKKMSSU R0668>>@.2DHX2XN c4 GGG$M1;M41PVXI9==,2399;H)--
39r:@@BO #OD8_56 /A779	*Q 23'!./ GGK(M-&# 	D!%%d+E%&:.4"5;;=I	 ww12"D)&56177966) 	Ii.Y]]628b9??AJ0Z5E5E5Gw5VY]]:.4"5;;=B		 W  @ 7s   BK2 L2	K?>K?c                    | xs dj                         j                         }|dk(  ry|sy	 ddlm}  ||      dk(  S # t        $ r Y yw xY w)zBTrue when the active provider likely fronts a local Ollama server.rY   ollamaTFr   )detect_local_server_type)rP   rQ   agent.model_metadatar   r~   )rX   r{   rn   r   s       r   _should_probe_ollama_visionr   C  sW    	R &&(AH}A'1X== s   > 	A
	A
c                v    t        | t              sy| j                         j                         }|t        v r|S y)z5Normalize a config value into one of the valid modes.r
   )r)   r*   rP   rQ   _VALID_MODES)r@   vals     r   _coerce_moder   R  s3    c3
))+


C
l
r%   c                   t        | t              sy| j                  d      xs i }t        |t              sy|j                  d      xs i }t        |t              syt        |j                  d      xs d      j	                         j                         }t        |j                  d      xs d      j	                         }t        |j                  d      xs d      j	                         }|dv r|s|syy	)
uc  True when the user configured a specific auxiliary vision backend.

    An explicit override means the user has a dedicated vision backend
    available; it's used as a *fallback* when the main model can't take
    images natively. In ``auto`` mode, native vision on a vision-capable
    main model still wins over this fallback — see issue #29135.
    F	auxiliaryvisionrX   rY   rV   r{   >   rY   r
   T)r)   r_   r`   r*   rP   rQ   )rf   auxr   rX   rV   r{   s         r   _explicit_aux_vision_overrider   \  s     c4 
''+

$"Cc4 WWX$"Ffd#6::j)/R0668>>@H

7#)r*002E6::j)/R0668H <hr%   c                   t        || |      }||S | r|syd}	 ddlm}  || |      }|t        |j                        S t        ||       }|s'| xs dj                         j                         dk(  rd}t        | |      r	 ddlm}  |||      }	|	|	S 	 yy# t        $ r"}t        j                  d| ||       Y d}~d}~ww xY w# t        $ r"}t        j                  d	| ||       Y d}~yd}~ww xY w)
a  Return True/False if we can resolve caps, None if unknown.

    Consults the user's ``supports_vision`` override in config.yaml first
    (so custom/local models declared as vision-capable don't fall through to
    text routing in ``auto`` mode), then falls back to models.dev.
    Nr   )get_model_capabilitiesu2   image_routing: caps lookup failed for %s:%s — %srY   r   zhttp://localhost:11434/v1)query_ollama_supports_visionu:   image_routing: ollama vision probe failed for %s:%s — %s)rx   agent.models_devr   r~   loggerdebugr(   rW   r   rP   rQ   r   r   r   )
rX   rV   rf   overridecapsr   excr{   r   ollama_visions
             r   _lookup_supports_visionr   w  s    )h>H5Da;%h6 D(())*39HR..0668HD."8X6	I8IM($$ ) -  aI8UZ\_``a  	LLL	  	s/   B C 	C!B>>C	C1C,,C1c                    d}t        |t              r?|j                  d      xs i }t        |t              rt        |j                  d            }|dk(  ry|dk(  ryt	        | ||      }|du ryt        |      ryy)a1  Return ``"native"`` or ``"text"`` for the given turn.

    Args:
      provider: active inference provider ID (e.g. ``"anthropic"``, ``"openrouter"``).
      model:    active model slug as it would be sent to the provider.
      cfg:      loaded config.yaml dict, or None. When None, behaves as auto.
    r
   agentimage_input_moder   r   T)r)   r_   r`   r   r   r   )rX   rV   rf   mode_cfg	agent_cfgsupportss         r   decide_image_input_moder     s     H#tGGG$*	i&#IMM2D$EFH86 'x<H4$S)r%   c                   | sy| j                  d      ry| j                  d      ry| dd dv ryt        |       d	k\  r| dd
 dk(  r	| dd	 dk(  ry| j                  d      ryt        |       d	k\  r| d
d dk(  r| dd	 }|dv ry|dv ry| dd
 dv ry| dd
 dk(  ry| dd j                         j                         }|j                  d      s|j                  d      rd|v ryy)a  Detect image MIME from magic bytes. Returns None if unrecognised.

    Filename-based detection (``mimetypes.guess_type``) is unreliable when
    upstream platforms lie about content-type. Discord, for example, can
    serve a PNG with ``content_type=image/webp`` for proxied/animated
    stickers, custom emoji previews, or images uploaded via certain bots.
    Anthropic strictly validates that declared media_type matches the
    actual bytes and returns HTTP 400 on mismatch, so we sniff to be safe.
    Ns   PNG

	image/pngs   
image/jpeg   >      GIF87a   GIF89a	image/gif      s   RIFF   s   WEBP
image/webps   BM	image/bmps   ftyp>      avif   avisz
image/avif>      heic   heim   heis   heix   hevc   hevx   mif1   msf1z
image/heic>      II*    MM *z
image/tiffs      zimage/x-iconi   s   <?xmls   <svgzimage/svg+xml)ra   rb   r   rQ   )r@   brandheads      r   _sniff_mime_from_bytesr     s    
~~*+
~~o&
2Aw((
3x2~#bq'W,Qrg1E
~~e
3x2~#a(g-Ab	&& 
 
  
2Aw**
2Aw%%t9##%Dx DOOG$<d?"r%   >   r   r   r   r   c                   	 ddl m} 	 ddl}|j                          	 ddl}	 ddl	m
} |j                   ||             5 }|j                  dvr|j                  d      } |       }|j                  |dd	
       |j                         cddd       S # t        $ r t        j	                  d       Y yw xY w# t        $ r Y w xY w# t        $ r Y w xY w# 1 sw Y   yxY w# t        $ r }t        j	                  d|       Y d}~yd}~ww xY w)a  Decode arbitrary image bytes with Pillow and re-encode as PNG.

    Returns None if Pillow isn't installed or can't decode the input
    (rare formats, corrupted bytes, missing optional decoder plugin for
    HEIC/AVIF, or vector formats like SVG). Caller falls back to skipping
    the image so the rest of the turn still works.

    HEIC/HEIF and AVIF need optional Pillow plugins; we try to register
    them on demand and swallow ImportError so a missing plugin just
    looks like 'Pillow can't decode this' rather than crashing.
    r   )Imagezimage_routing: Pillow not installed; cannot transcode non-standard image format to PNG. Install with `pip install Pillow` (and `pillow-heif` / `pillow-avif-plugin` for those formats).N)BytesIO>   LPLARGBRGBAr   PNGF)formatoptimizez<image_routing: Pillow could not transcode image to PNG -- %s)PILr   ImportErrorr   infopillow_heifregister_heif_openerr~   pillow_avifior   openmodeconvertsavegetvalue)r@   r   r   r   r   imbufr   s           r   _transcode_to_pngr     s   ((*ZZ% 	" ww==ZZ')CGGCG6<<>	" 	"-  L	

     
	" 	"  JC	
 	si   B B8 C C"  A
C
	C" B54B58	CC	CCCC" C" "	D+DDc                    |t        |      }|r|S t        j                  t        |             \  }}|r|j	                  d      r|S | j
                  j                         }dddddddj                  |d      S )zReturn image MIME type for *path*.

    If *raw* bytes are provided, magic-byte sniffing wins (authoritative).
    Otherwise we fall back to ``mimetypes`` then suffix-based defaults.
    zimage/r   r   r   r   r   )r   r   r   r   r   r   )r   	mimetypes
guess_typer*   ra   suffixrQ   r`   )r5   r@   sniffedmime_r   s         r   _guess_mimer   M  s     (-N""3t9-GD!) [[ F 
c&, r%   c                2   	 ddl m}  |t        |              	 | j                         }t        | |      }|t        vrJt        |      }|t        j                  d| |       yt        j                  d| j                  |       |}d	}t        j                  |      j                  d
      }d| d| S # t        $ r!}t        j                  d| |       Y d}~yd}~wt        $ r Y w xY w# t        $ r!}t        j                  d| |       Y d}~yd}~ww xY w)uW  Encode a local image as a base64 data URL at its native size.

    Size limits are NOT enforced here — the agent retry loop
    (``run_agent._try_shrink_image_parts_in_messages``) shrinks on the
    provider's first rejection. Keeping this simple means providers that
    accept large images (OpenAI 49 MB+, Gemini 100 MB) don't pay a silent
    quality tax just because one other provider is stricter.

    Format compatibility IS handled here: if the sniffed MIME isn't one
    of ``_UNIVERSALLY_SUPPORTED_MIMES`` (i.e. it's something like AVIF,
    HEIC, BMP, TIFF, or ICO that some providers reject outright), we
    transcode to PNG with Pillow before declaring media_type. This fixes
    the user-visible "Could not process image" HTTP 400 from Anthropic on
    Discord-attached AVIF/HEIC/BMP files.

    Returns None if the file can't be read OR if the format isn't
    universally supported AND Pillow can't transcode it (Pillow missing,
    HEIC/AVIF plugin missing, vector format like SVG, corrupt bytes). The
    caller reports those paths in ``skipped`` and the rest of the turn
    proceeds.
    r   )raise_if_read_blockedz6image_routing: blocked local image attachment %s -- %sNu'   image_routing: failed to read %s — %s)r@   zimage_routing: %s is %s which is not accepted by all major vision providers and could not be transcoded to PNG; skipping this attachment.zIimage_routing: transcoded %s (%s) -> image/png for provider compatibilityr   asciizdata:z;base64,)agent.file_safetyr   r*   
ValueErrorr   warningr~   
read_bytesr   _UNIVERSALLY_SUPPORTED_MIMESr   r   r^   base64	b64encodedecode)r5   r   r   r@   r   
transcodedb64s          r   _file_to_data_urlr   g  s   ,	;c$i(oo t%D//&s+
NN, d	 WIIt	
 


3

&
&w
/C4&&&=  OQUWZ[   @$Ls4   B5 C, 5	C)>CC)(C),	D5DDc                *   g }g }g }g }|D ]  }t        |      }|j                         r|j                         s|j                  t	        |             It        |      }	|	s|j                  t	        |             q|j                  dd|	id       |j                  t	        |              |xs g D ]@  }
|
xs dj                         }
|
s|j                  dd|
id       |j                  |
       B | xs dj                         }|s|rj|xs d}g }|j                  d |D               |j                  d |D               | dd	j                  |      z   }d
|dg}|j                  |       ||fS g }|r|j                  d
|d       ||fS )u  Build an OpenAI-style ``content`` list for a user turn.

    Shape:
      [{"type": "text", "text": "...\n\n[Image attached at: /local/path]"},
       {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}},
       {"type": "image_url", "image_url": {"url": "https://example.com/a.png"}},
       ...]

    Local paths are read from disk and embedded as base64 ``data:`` URLs.
    Remote URLs (``http(s)://``) are passed through verbatim — the provider
    fetches them server-side. The model still sees the pixels either way.

    For each successfully attached image, a hint is appended to the text
    part:

      * local path → ``[Image attached at: <path>]``
      * URL        → ``[Image attached: <url>]``

    The hint gives the model a string handle so MCP/skill tools that take
    an image path or URL argument can be invoked on the same image without
    an extra round-trip. This parallels the text-mode hint produced by
    ``Runner._enrich_message_with_vision`` (``vision_analyze using image_url:
    <path>``) so behaviour is consistent across both image input modes.

    Images are attached at their native size. If a provider rejects the
    request because an image is too large (e.g. Anthropic's 5 MB per-image
    ceiling), the agent's retry loop transparently shrinks and retries
    once — see ``run_agent._try_shrink_image_parts_in_messages``.

    Returns (content_parts, skipped). Skipped entries are local paths
    that couldn't be read from disk; URLs are never skipped (they're
    not validated here).
    	image_urlrD   )typer   rY   zWhat do you see in this image?c              3  (   K   | ]
  }d | d  yw)z[Image attached at: ]Nr   )r   rn   s     r   r   z-build_native_content_parts.<locals>.<genexpr>  s     N!015N   c              3  (   K   | ]
  }d | d  yw)z[Image attached: r   Nr   )r   us     r   r   z-build_native_content_parts.<locals>.<genexpr>  s     Jq-aS2Jr   z


r   )r   r   )	r   existsis_filer.   r*   r   rP   extendjoin)	user_textimage_paths
image_urlsskippedimage_partsattached_pathsattached_urlsraw_pathrn   data_urlrD   r   	base_text
hint_linescombined_textpartss                   r   build_native_content_partsr    s   L G(*K "N!M -NxxzNN3x=)$Q'NN3x=)*
 	 	c(m,- R "yb!
 	 	S!" O""$D <<	 "
N~NNJMJJ$+T*TYYz-BB06'N&O[!g~ Efd34'>r%   )r   r  rE   )r   r*   r'   zTuple[List[str], List[str]])r@   r   r'   Optional[bool])rf   Optional[Dict[str, Any]]rX   r*   rV   r*   r'   r  )rf   r  rX   r*   r'   r*   )rX   r*   r{   r*   r'   r(   )r@   r   r'   r*   )rf   r  r'   r(   r   )rX   r*   rV   r*   rf   r  r'   r  )rX   r*   rV   r*   rf   r  r'   r*   )r@   bytesr'   Optional[str])r@   r  r'   Optional[bytes])r5   r   r@   r  r'   r*   )r5   r   r'   r  )r   r*   r   z	List[str]r  zOptional[List[str]]r'   z&Tuple[List[Dict[str, Any]], List[str]]).__doc__
__future__r   r   loggingr   r4   r+   pathlibr   typingr   r   r   r   r	   	getLogger__name__r   	frozensetr   _IMAGE_EXTSr   _IMAGE_EXT_PATTERNcompile
IGNORECASEr2   r:   rE   rR   rS   rT   rx   r   r   r   r   r   r   r   r   r   r   r   r  __all__r   r%   r   <module>r     s  $L #    	 	  3 3			8	$ 34 XXA[AA 
 "rzz69KKfTMM  

!$669OOMMBZ 3456"Q	!QQ Q 	Qh8	!88 	8v< %)*** 
"* 	*Z    
"  		 d0|  ) *   
1h 48'| '+VVV $V ,	Vrr%   