
    `gjJ                        d Z ddlmZ ddlZddlZddlZddlmZ  ej                  e	      Z
 ej                  d      ZddZddZddZdd	Zddd
ZdddZddZddZddZddZddZg dZy)at  Message and tool-payload sanitization helpers.

Pure functions extracted from ``run_agent.py`` so the AIAgent module can
stay focused on the conversation loop.  These walk OpenAI-format message
lists and structured payloads, repairing or stripping problematic
characters that would otherwise crash ``json.dumps`` inside the OpenAI
SDK or be rejected by upstream APIs.

All helpers are stateless and side-effect-free except for in-place
mutation of their input (where documented).  Backward-compatible
re-exports from ``run_agent`` remain in place so existing imports
``from run_agent import _sanitize_surrogates`` keep working.
    )annotationsN)Anyz[\ud800-\udfff]c                \    t         j                  |       rt         j                  d|       S | S )zReplace lone surrogate code points with U+FFFD (replacement character).

    Surrogates are invalid in UTF-8 and will crash ``json.dumps()`` inside the
    OpenAI SDK.  This is a fast no-op when the text contains no surrogates.
       �)_SURROGATE_REsearchsubtexts    M/root/.hermes/venv/lib/python3.12/site-packages/agent/message_sanitization.py_sanitize_surrogatesr      s*     D!  400K    c                *    dfd |        S )uu  Replace surrogate code points in nested dict/list payloads in-place.

    Mirror of ``_sanitize_structure_non_ascii`` but for surrogate recovery.
    Used to scrub nested structured fields (e.g. ``reasoning_details`` — an
    array of dicts with ``summary``/``text`` strings) that flat per-field
    checks don't reach.  Returns True if any surrogates were replaced.
    Fc                $   t        | t              rz| j                         D ]f  \  }}t        |t              r2t        j                  |      s,t        j                  d|      | |<   dHt        |t        t        f      s_ |       h y t        | t              rut        |       D ]f  \  }}t        |t              r2t        j                  |      s,t        j                  d|      | |<   dHt        |t        t        f      s_ |       h y y )Nr   T)	
isinstancedictitemsstrr   r   r	   list	enumerate)nodekeyvalueidx_walkfounds       r   r   z-_sanitize_structure_surrogates.<locals>._walk4   s    dD!"jjl !
UeS)$++E2$1$5$5h$FS	 $d|4%L! d#'o !
UeS)$++E2$1$5$5h$FS	 $d|4%L! $r    payloadr   r   s    @@r   _sanitize_structure_surrogatesr    *   s     E!& 
'NLr   c                *   d}| D ]
  }t        |t              s|j                  d      }t        |t              r1t        j                  |      rt        j                  d|      |d<   d}n{t        |t              rk|D ]f  }t        |t              s|j                  d      }t        |t              s6t        j                  |      sLt        j                  d|      |d<   d}h |j                  d      }t        |t              r0t        j                  |      rt        j                  d|      |d<   d}|j                  d      }t        |t              r2|D ],  }t        |t              s|j                  d      }	t        |	t              r0t        j                  |	      rt        j                  d|	      |d<   d}|j                  d	      }
t        |
t              s|
j                  d      }t        |t              r0t        j                  |      rt        j                  d|      |
d<   d}|
j                  d
      }t        |t              st        j                  |      st        j                  d|      |
d
<   d}/ |j                         D ]q  \  }}|dv rt        |t              r2t        j                  |      s1t        j                  d|      ||<   d}Mt        |t        t        f      sdt        |      spd}s  |S )a  Sanitize surrogate characters from all string content in a messages list.

    Walks message dicts in-place. Returns True if any surrogates were found
    and replaced, False otherwise. Covers content/text, name, tool call
    metadata/arguments, AND any additional string or nested structured fields
    (``reasoning``, ``reasoning_content``, ``reasoning_details``, etc.) so
    retries don't fail on a non-content field.  Byte-level reasoning models
    (xiaomi/mimo, kimi, glm) can emit lone surrogates in reasoning output
    that flow through to ``api_messages["reasoning_content"]`` on the next
    turn and crash json.dumps inside the OpenAI SDK.
    Fcontentr   Tr   name
tool_callsidfunction	arguments>   r#   roler"   r$   )
r   r   getr   r   r   r	   r   r   r    )messagesr   msgr"   partr   r#   r$   tctc_idfnfn_namefn_argsr   r   s                  r   _sanitize_messages_surrogatesr2   K   s    E 3!#t$'')$gs#(<(<W(E*..xAC	NE& %dD)88F+D!$,1E1Ed1K'4'8'84'HV $% wwvdC ]%9%9$%?'++Hd;CKEWW\*
j$'  %!"d+teS)m.B.B5.I,005ABtH EVVJ'b$' ffVnG!'3/M4H4H4Q%2%6%6x%I6
 $ ff[1G!'3/M4H4H4Q*7*;*;Hg*N; $!%, ))+ 		!JC??%% ''.,005ACH EED$<01%8 E		!U3!h Lr   c                   g }d}d}t        |       }||k  r| |   }|r|dk(  r6|dz   |k  r.|j                  |       |j                  | |dz             |dz  }G|dk(  rd}|j                  |       nWt        |      dk  r|j                  dt        |      d	       n*|j                  |       n|dk(  rd
}|j                  |       |dz  }||k  rdj                  |      S )ug  Escape unescaped control chars inside JSON string values.

    Walks the raw JSON character-by-character, tracking whether we are
    inside a double-quoted string. Inside strings, replaces literal
    control characters (0x00-0x1F) that aren't already part of an escape
    sequence with their ``\uXXXX`` equivalents. Pass-through for everything
    else.

    Ported from #12093 — complements the other repair passes in
    ``_repair_tool_call_arguments`` when ``json.loads(strict=False)`` is
    not enough (e.g. llama.cpp backends that emit literal apostrophes or
    tabs alongside other malformations).
    Fr   \      "    z\u04xT )lenappendordjoin)rawout	in_stringinchs         r   %_escape_invalid_chars_in_json_stringsrE      s     CI	ACA
a%VTza!eai

2

3q1u:&QSy!	

2R4

SR./

2Sy 	JJrN	Q) a%* 773<r   c                   t        | t              r| j                         nd}|st        j	                  d|       y|dk(  rt        j	                  d|       y	 t        j                  |d      }t        j                  |d	      }||k7  rt        j	                  d
|       |S # t
        j                  t        t        f$ r Y nw xY w|}t        j                  dd|      }|j                  d      |j                  d      z
  }|j                  d      |j                  d      z
  }|dkD  r|d|z  z  }|dkD  r|d|z  z  }t        d      D ]  }	 t        j                  |        n# t
        j                  $ rz |j                  d      r)|j                  d      |j                  d      kD  r|dd }n=|j                  d      r)|j                  d      |j                  d      kD  r|dd }nY  nY w xY w 	 t        j                  |       t        j	                  d||dd |dd        |S # t
        j                  $ r Y nw xY w	 t!        |      }	|	|k7  r5t        j                  |	       t        j	                  d||dd |	dd        |	S n$# t
        j                  t        t        f$ r Y nw xY wt        j	                  d||dd        y)a  Attempt to repair malformed tool_call argument JSON.

    Models like GLM-5.1 via Ollama can produce truncated JSON, trailing
    commas, Python ``None``, etc.  The API proxy rejects these with HTTP 400
    "invalid tool call arguments".  This function applies common repairs;
    if all fail it returns ``"{}"`` so the request succeeds (better than
    crashing the session).  All repairs are logged at WARNING level.
    r:   z*Sanitized empty tool_call arguments for %sz{}Nonez0Sanitized Python-None tool_call arguments for %sF)strict),:)
separatorsz>Repaired unescaped control chars in tool_call arguments for %sz,\s*([}\]])z\1{}[]r   2   Nu8   Repaired malformed tool_call arguments for %s: %s → %sP   uA   Repaired control-char-laced tool_call arguments for %s: %s → %suP   Unrepairable tool_call arguments for %s — replaced with empty object (was: %s))r   r   striploggerwarningjsonloadsdumpsJSONDecodeError	TypeError
ValueErrorrer	   countrangeendswithrE   )
raw_args	tool_nameraw_strippedparsedreserialisedfixed
open_curlyopen_bracket_escapeds
             r   _repair_tool_call_argumentsrj      s    (2(C'@8>>#bL CYO vI9U
L7zz&Z@<'NNP   )Z8  EFF>5%0ES!EKK$44J;;s#ekk#&66LA~z!!a|##2Y 
		JJu## 	~~c"u{{3'7%++c:J'Jcr
$S)9EKK<L)Lcr
		


5F|CR(%*	
  
7>eJJwNNS<,gcrl N    )Z8 
 NN	/<$
 sK   A
B$ $CCE''BG43G494H. .IIAJ J/.J/c                    | sy| d   }t        |t              r|j                  d      dk7  ryt        |t              r|nd}| j	                  d|j                         xs dd       y	)
u"  Append a synthetic assistant turn when an interrupted tail is a tool result.

    A turn cut short by ``/stop`` can leave the transcript ending on a raw
    ``tool`` message (a tool finished, or its execution was cancelled, but the
    model never streamed a closing assistant turn). Persisting that tail means
    the next user message lands as ``… tool → user`` — a role-alternation
    violation that strict providers (Gemini, Claude) react to by hallucinating
    a continuation of the user's message and ignoring prior context, which
    reads to the user as "lost context" (#48879).

    ``finalize_turn`` closes this on the happy interrupt path, but the
    retry/backoff/error interrupt aborts in ``conversation_loop`` ``return``
    early and never reach it — this shared helper closes the sequence on all of
    them. ``final_response`` is usually empty on an interrupt, so an explicit
    placeholder is used rather than an empty-content assistant turn.

    Mutates ``messages`` in place. Returns True if a closing turn was appended.
    FrQ   r(   toolr:   	assistantzOperation interrupted.)r(   r"   T)r   r   r)   r   r<   rS   )r*   final_responselastr   s       r   close_interrupted_tool_sequencerp     sh    & B<DdD!TXXf%5%?'<>"DOO::<;#;  r   c                F    | j                  dd      j                  d      S )zRemove non-ASCII characters, replacing with closest ASCII equivalent or removing.

    Used as a last resort when the system encoding is ASCII and can't handle
    any non-ASCII characters (e.g. LANG=C on Chromebooks).
    asciiignore)errors)encodedecoder
   s    r   _strip_non_asciirw   :  s"     ;;wx;077@@r   c                   d}| D ]  }t        |t              s|j                  d      }t        |t              rt	        |      }||k7  rj||d<   d}nbt        |t
              rR|D ]M  }t        |t              s|j                  d      }t        |t              s6t	        |      }||k7  sG||d<   d}O |j                  d      }t        |t              rt	        |      }||k7  r||d<   d}|j                  d      }t        |t
              ru|D ]p  }	t        |	t              s|	j                  di       }
t        |
t              s7|
j                  d      }t        |t              sYt	        |      }||k7  sj||
d<   d}r |j                         D ]3  \  }}|d	v rt        |t              st	        |      }||k7  s-|||<   d}5  |S )
a  Strip non-ASCII characters from all string content in a messages list.

    This is a last-resort recovery for systems with ASCII-only encoding
    (LANG=C, Chromebooks, minimal containers).  Returns True if any
    non-ASCII content was found and sanitized.
    Fr"   Tr   r#   r$   r&   r'   >   r#   r(   r"   r$   )r   r   r)   r   rw   r   r   )r*   r   r+   r"   	sanitizedr,   r   r#   r$   r-   r/   r1   r   r   s                 r   _sanitize_messages_non_asciirz   C  s    E /!#t$'')$gs#(1IG#!*I& )dD)88F+D!$,$4T$:	$,+4DL$(E) wwvdC (.ID 'FWW\*
j$'  	-b$'
B/B!"d+"$&&"5%gs3(8(AI(G32;;(,	- ))+ 	!JC??%%,U3	%(CH E	!Q/!` Lr   c                    t        |       S )z7Strip non-ASCII characters from tool payloads in-place.)_sanitize_structure_non_ascii)toolss    r   _sanitize_tools_non_asciir~   ~  s    (//r   c                   d}g }t        |       D ]  \  }}t        |t              s|j                  d      }t        |t              s9g }|D ]9  }t        |t              r|j                  d      dv rd})|j                  |       ; t        |      t        |      k  s|r||d<   |j                  d      dk(  rd|d<   |j                  |        t        |      D ]  }| |=  |S )	uB  Remove image_url content parts from all messages in-place.

    Called when a server signals it does not support images (e.g.
    "Only 'text' content type is supported.").  Mutates messages so the
    next API call sends text only.

    Preserves message alternation invariants:
      * ``tool``-role messages whose content was entirely images are replaced
        with a plaintext placeholder, NOT deleted — deleting them would leave
        the paired ``tool_call_id`` on the prior assistant message unmatched,
        which providers reject with HTTP 400.
      * Non-tool messages whose content becomes empty are dropped.  In
        practice this only hits synthetic image-only user messages appended
        for attachment delivery; real user turns always include text.

    Returns True if any image parts were removed.
    Fr"   type>   image	image_urlinput_imageTr(   rl   u:   [image content removed — server does not support images])r   r   r   r)   r   r<   r;   reversed)r*   r   	to_deleterB   r+   r"   	new_partsr,   s           r   _strip_images_from_messagesr     s    $ EIH% $3#t$'')$'4(	 	'D$%$((6*:>c*c  &		'
 y>CL(!*IF* "^I   #-$. i  QKLr   c                *    dfd |        S )zCStrip non-ASCII characters from nested dict/list payloads in-place.Fc                   t        | t              ra| j                         D ]M  \  }}t        |t              rt	        |      }||k7  s'|| |<   d/t        |t        t
        f      sF |       O y t        | t
              r\t        |       D ]M  \  }}t        |t              rt	        |      }||k7  s'|| |<   d/t        |t        t
        f      sF |       O y y )NT)r   r   r   r   rw   r   r   )r   r   r   ry   r   r   r   s        r   r   z,_sanitize_structure_non_ascii.<locals>._walk  s    dD!"jjl !
UeS) 0 7I E)$-S	 $d|4%L! d#'o !
UeS) 0 7I E)$-S	 $d|4%L! $r   r   r   s    @@r   r|   r|     s    E!* 
'NLr   )r   rp   r   r    r2   rE   rj   rw   rz   r~   r   r|   )r   r   returnr   )r   r   r   bool)r*   r   r   r   )r?   r   r   r   )?)r`   r   ra   r   r   r   )N)r*   r   rn   r   r   r   )r}   r   r   r   )__doc__
__future__r   rV   loggingr\   typingr   	getLogger__name__rT   compiler   r   r    r2   rE   rj   rp   rw   rz   r~   r   r|   __all__r   r   r   <module>r      s    #   	 			8	$ 

-.BAH'T^B@A8v0
-`:r   