
    sj_                     *   d Z ddlZddlmZmZmZmZ dZdZdZ	dZ
defd	Zd
ee         dee         fdZdedefdZd Zdee         fdZdefdZdefdZdedefdZdee         dededefdZdee         dedefdZ	 	 	 	 d8ded
eee                  dedeee                  dee         defdZdefdZd d!d"d#d$d%d&d'd#ied(d)d*d+d%d&ed,d"d#d-d%d'd#id&d'd#ied.d'd*id/dgd0d1d2dgd0d3Zdd4lmZmZ  ej         d d ed5 ed67           dS )9a^  
Clarify Tool Module - Interactive Clarifying Questions

Allows the agent to present structured multiple-choice questions or open-ended
prompts to the user. In CLI mode, choices are navigable with arrow keys. On
messaging platforms, choices are rendered as a numbered list.

Supports both single-select (radio) and multi-select (checkbox) modes via the
``multi_select`` parameter.

The actual user-interaction logic lives in the platform layer (cli.py for CLI,
gateway/run.py for messaging). This module defines the schema, validation, and
a thin dispatcher that delegates to a platform-provided callback.
    N)DictListOptionalCallable      zrThe user did not provide a response within the time limit. Use your best judgement to make the choice and proceed.z(Recommended)returnc                    | dS t          | t                    r|                                 S t          | t                    r[dD ]V}|                     |          }t          |t                    r*|                                r|                                c S WdS t          | t
          t          f          r1d                    d | D                                                       S t          |                                           S )u  Coerce a single choice into its user-facing display string.

    The schema declares choices as bare strings, but LLMs sometimes emit
    dict-shaped choices like ``[{"description": "..."}]``. A naive ``str(c)``
    turns the whole dict into its Python repr — ``{'description': '...'}`` —
    which then leaks onto every surface that renders the choice (CLI panel,
    Discord buttons, Telegram numbered list) AND is returned verbatim as the
    user's answer. Normalising here, at the one platform-agnostic entry point,
    fixes the whole class in one place instead of per-adapter.

    Dict unwrap order is the canonical LLM tool-call user-facing keys:
    ``label`` → ``description`` → ``text`` → ``title``. ``name`` and ``value``
    are deliberately excluded — they're component-shaped fields that could
    carry raw enum values or short identifiers, not human-readable labels. A
    dict with none of the canonical keys is dropped (returns ""), since a
    garbage label is worse than no choice at all.
    N )labeldescriptiontexttitle c              3   4   K   | ]}t          |          V  d S N_flatten_choice).0xs     6/home/agent/.hermes/hermes-agent/tools/clarify_tool.py	<genexpr>z"_flatten_choice.<locals>.<genexpr>H   s*      66q**666666    )
isinstancestrstripdictgetlisttuplejoin)ckeyvs      r   r   r   +   s    $ 	yr!S wwyy!T < 	! 	!Cc

A!S!! !aggii !wwyy   r!dE]## ?xx66A66666<<>>>q66<<>>r   choicesc                     t          |           dk     r| S t          | d                                                   }|t          |          k    r| S | dt           gt          | dd                   z   S )u  Label the first choice as the agent's recommendation.

    The schema tells the model to order ``choices`` best-first, so element 0 is
    always the option it would pick itself. Tagging it here — the one
    platform-agnostic entry point — means every surface (CLI panel, TUI,
    desktop card, Telegram buttons) reads the same way without four copies of
    the same string concatenation, and the label can never drift between them.

    Idempotent: a model that writes its own "(recommended)" into the choice is
    left alone rather than getting the suffix twice. A lone choice isn't a
    recommendation — there's nothing to prefer it over — so single-choice lists
    pass through untouched.
       r   r      N)lenr   r   strip_recommendedRECOMMENDED_LABELr   )r%   firsts     r   mark_recommendedr-   L   s{     7||a
OO!!##E!%((((**(**+d7122;.?.???r   r   c                 "   t          |                                           }|                                                    t                                                    r/|dt          t                                                              S |S )uB  Remove the recommendation label from a resolved answer.

    The user picks the decorated string, but the agent asked about the bare
    option — returning "Rebase onto main (Recommended)" as ``user_response``
    would leak presentation into the answer the model reasons about and into
    anything it echoes back.
    N)r   r   casefoldendswithr+   r)   )r   strippeds     r   r*   r*   b   sv     4yy  H##$5$>$>$@$@AA ;1301111288:::Or   c                    ddl d}	                     |           }|j        }d|v p,t          fd|                                D                       }n# t
          t          f$ r d}Y nw xY w|r | |||          S  | ||          S )ub  Invoke the platform callback, passing multi_select if supported.

    Uses signature inspection (not a ``TypeError`` retry) to decide whether
    the callback accepts the ``multi_select`` keyword — a retry-on-TypeError
    approach would re-invoke a *compatible* callback that raised TypeError
    internally, potentially prompting the user twice.
    r   NFmulti_selectc              3   B   K   | ]}|j         j        j        k    V  d S r   kind	ParameterVAR_KEYWORDr   pinspects     r   r   z#_invoke_callback.<locals>.<genexpr>~   sA       8
 8
89AFg'338
 8
 8
 8
 8
 8
r   )r3   r;   	signature
parametersanyvalues	TypeError
ValueError)callbackquestionr%   r3   accepts_multisigparamsr;   s          @r   _invoke_callbackrH   p   s     NNNM	))&&0 
C 8
 8
 8
 8
=C]]__8
 8
 8
 5
 5
 z"    
  Fx'EEEE8Hg&&&s   AA A-,A-c                    t          | t                    rd | D             S t          |                                           }|                    d          rL	 t          j        |          }t          |t                    rd |D             S n# t
          j        $ r Y nw xY wd |                    d          D             S )u  Parse a multi-select response into a list of cleaned choice strings.

    Handles three forms:
      - Already a list  →  stringify + strip each element
      - JSON array      →  parse and strip
      - Comma-separated →  split, strip, drop empties
    c                     g | ]D}t          |                                          #t          |                                          ES  r   r   r   rs     r   
<listcomp>z0_parse_multi_select_response.<locals>.<listcomp>   s9    GGG1AGAGGGr   [c                     g | ]D}t          |                                          #t          |                                          ES rK   rL   )r   r:   s     r   rO   z0_parse_multi_select_response.<locals>.<listcomp>   s9    III1#a&&,,..IAIIIr   c                 ^    g | ]*}|                                 |                                 +S rK   )r   r   ss     r   rO   z0_parse_multi_select_response.<locals>.<listcomp>   s-    ;;;!;AGGII;;;r   ,)	r   r   r   r   
startswithjsonloadsJSONDecodeErrorsplit)raw_responserawparseds      r   _parse_multi_select_responser^      s     ,%% HGGGGGG
l


!
!
#
#C ~~c 	Z__F&$'' JIIIIIIJ# 	 	 	D	 <;syy~~;;;;s   4B B! B!c                    t          | t                    sdS | sdS t          |           t          k    rddt           dfS g }t	          |           D ]\  }}t          |t
                    rd|i}t          |t                    s
dd| dfc S t          |                    d          pd	                                          }|s
dd| d
fc S |                    d          }|`t          |t                    s
dd| dfc S d d |D             D             }t          |          t          k    r|dt                   }|sd}t          |                    d          pd	                                          pd}|
                    d| |||rt          t          |                    nd|rt          |          ndt          |                    d                    ot          |          d           |dfS )u  Validate and normalize the ``questions`` batch parameter.

    Returns ``(normalized, error)`` where exactly one is non-None, except the
    empty-list case which returns ``(None, None)`` — an empty array is not an
    error, it just means "no batch here" and the caller falls back to the
    single-question path.

    Each normalized entry carries:
      - ``qid``: stable wire id (``q0``..``qN``, index order). Surfaces key
        their per-question answers by this; a model-supplied ``id`` is NOT
        used on the wire (it's unvalidated text) and only echoed in results.
      - ``id``: the model's optional identifier, or None.
      - ``question``: stripped question text.
      - ``choices``: decorated choice list (recommended label applied), or
        None for open-ended.
      - ``choices_offered``: the bare list as offered, for the result JSON.
      - ``multi_select``: honored only when choices exist.
    )Nz/questions must be an array of question objects.)NNNzquestions supports at most z items.rD   z
questions[z&] must be an object with a 'question'.r   z"].question must be non-empty text.r%   z].choices must be a list.c                     g | ]}||S rK   rK   rS   s     r   rO   z(_normalize_questions.<locals>.<listcomp>   s    MMMQ1MqMMMr   c              3   4   K   | ]}t          |          V  d S r   r   r   r"   s     r   r   z'_normalize_questions.<locals>.<genexpr>   s*      "G"G!?1#5#5"G"G"G"G"G"Gr   idqr3   )qidrc   rD   r%   choices_offeredr3   )r   r   r)   MAX_QUESTIONS	enumerater   r   r   r   MAX_CHOICESappendr-   bool)	questions
normalizedindexitemr   r%   model_ids          r   _normalize_questionsrq      sc   & i&& GFF z
9~~%%I=IIIIIJ ++  tdC   	&%D$%% 	TSeSSSSSSS488J''-2..4466 	POeOOOOOOO((9%%gt,, KJ%JJJJJJJMM"G"Gw"G"G"GMMMG7||k))!,;,/ txx~~+,,2244<u;;:AK'W666t07AtG}}}T .!9!9::LtG}}
 
 	 	 	 	 tr   c                     ddl 	                     |           j        }d|v p,t          fd|                                D                       S # t
          t          f$ r Y dS w xY w)u   True when the platform callback understands the ``questions`` kwarg.

    Same signature-inspection approach as ``_invoke_callback`` (never a
    TypeError retry — that would re-prompt the user on an internal bug).
    r   Nrl   c              3   B   K   | ]}|j         j        j        k    V  d S r   r5   r9   s     r   r   z._callback_accepts_questions.<locals>.<genexpr>   sA       ,
 ,
89AFg'33,
 ,
 ,
 ,
 ,
 ,
r   Fr<   )rC   rG   r;   s     @r   _callback_accepts_questionsrt      s     NNN""8,,7f$ 
 ,
 ,
 ,
 ,
=C]]__,
 ,
 ,
 )
 )
 	
 z"   uus   A
A A'&A'entryc                 b    | d         rd t          |          D             S t          |          S )zEStrip presentation from one locked answer (label, multi-select JSON).r3   c                 ,    g | ]}t          |          S rK   r*   rM   s     r   rO   z'_clean_batch_answer.<locals>.<listcomp>   s!    PPP!!$$PPPr   )r^   r*   )ru   r\   s     r   _clean_batch_answerry      s;    ^ QPP.J3.O.OPPPPS!!!r   rm   answers	timed_outc                 6   g }| D ]t}i }|d         r|d         |d<   |d         |d<   |d         |d<   |                     |d                   }|rt          ||          nd|d<   |                    |           ud|i}|rd|d	<   t          j        |d
          S )u  Assemble the batch result JSON from per-qid answers.

    Unanswered questions surface as empty ``user_response`` — with the
    top-level ``timed_out`` flag (present only when true) telling the agent
    whether those blanks are deliberate skips or the user walking away.
    rc   rD   rf   re   r   user_response	responsesTr{   Fensure_ascii)r   ry   rj   rW   dumps)rm   rz   r{   r~   ru   rowr\   results           r   _batch_resultr      s     I  ; 	$dCI
+J!&'8!9kk%,''BEM25#>>>2O!,i 8F #"{:f51111r   rD   c                    t          |          rh ||d|           }i }d}|2t          |t                    r!|                                t          k    rd}nt          |t
                    rGt          |                    d          pi           }t          |                    d                    }nt          |t                    r|                                r	 t          j	        |          }n# t          j
        $ r d}Y nw xY wt          |t
                    rFt          |                    d          pi           }t          |                    d                    }t          | ||          S i }d}| D ]i}t          ||d         |d         |d	                   }|2t          |t                    r!|                                t          k    rd} n|||d
         <   jt          | ||          S )u  Dispatch a validated batch to the platform callback.

    Batch-capable callbacks (a ``questions`` kwarg, detected by signature)
    get the whole list once and reply with ``{"answers": {qid: raw}}`` plus
    an optional ``timed_out`` flag — as a dict or a JSON string (the
    tui_gateway ``_block`` bridge can only carry strings).

    Legacy callbacks are looped one question at a time (messaging adapters,
    older plugins). An explicit empty answer is a skip and the loop
    continues; a timeout (``None`` or the ``TIMEOUT_RESPONSE`` sentinel)
    means the user walked away, so the loop aborts instead of pestering
    them with the remaining questions. Answers collected before the abort
    are kept either way.
    N)rl   FTrz   r{   rD   r%   r3   re   )rt   r   r   r   TIMEOUT_RESPONSEr   r   rk   rW   rX   rY   r   rH   )rm   rC   rD   r\   rz   r{   r]   ru   s           r   
_run_batchr     s    #8,, =hx<<<	;:c3//;CIIKKCS4S4SIIT"" 
	:3779--344GSWW[1122IIS!! 	:ciikk 	:C'   &$'' :vzz)44:;; K!8!899	 Z)<<<GI $ $eJ'y)95;P
 
 ;:c3//;CIIKKCS4S4SIE #eWi888s   !C6 6D
	D
Fr3   rl   rC   c                    |t          |          \  }}|rt          |          S |rm|t          d          S 	 t          ||t          | pd                                                    S # t
          $ r}t          d|           cY d}~S d}~ww xY w| r|                                 st          d          S |                                 } |et          |t                    st          d          S d d |D             D             }t          |          t          k    r|dt                   }|sd}|t          d          S |}|t          |          }	 t          || ||          }	n)# t
          $ r}t          d|           cY d}~S d}~ww xY w|r|d	 t          |	          D             }
nt          |	          }
t          j        | ||
d
d          S )an  
    Ask the user a question, optionally with multiple-choice options.

    Args:
        question:     The question text to present.
        choices:      Up to 4 predefined answer choices. When omitted the
                      question is purely open-ended.
        multi_select: When True, the user can select multiple choices
                      (checkboxes).  The ``user_response`` in the output JSON
                      will be a list of strings instead of a single string.
                      Has no effect when ``choices`` is omitted.
        questions:    Up to 5 independent questions asked as one batch
                      (issue #18450). Each item: ``{id?, question, choices?,
                      multi_select?}``. When present (non-empty), the single
                      ``question``/``choices``/``multi_select`` parameters
                      are ignored and the result JSON is ``{"responses":
                      [...]}`` (plus ``"timed_out": true`` when the user
                      stopped answering partway).
        callback:     Platform-provided function that handles the actual UI
                      interaction.  Signature:
                      ``callback(question, choices, multi_select=False) -> str``.
                      Batch-capable platforms additionally accept a
                      ``questions`` keyword and receive the normalized list
                      in one call; platforms without it are looped one
                      question at a time.
                      Injected by the agent runner (cli.py / gateway).

    Returns:
        JSON string with the user's response(s).
    Nz8Clarify tool is not available in this execution context.r   zFailed to get user input: zQuestion text is required.z"choices must be a list of strings.c                     g | ]}||S rK   rK   rS   s     r   rO   z clarify_tool.<locals>.<listcomp>  s    IIIqI1IIIr   c              3   4   K   | ]}t          |          V  d S r   r   rb   s     r   r   zclarify_tool.<locals>.<genexpr>  s*      CCaq11CCCCCCr   c                 ,    g | ]}t          |          S rK   rx   rM   s     r   rO   z clarify_tool.<locals>.<listcomp>  s!    bbb!*1--bbbr   )rD   rf   r}   Fr   )rq   
tool_errorr   r   r   	Exceptionr   r   r)   ri   r-   rH   r^   r*   rW   r   )rD   r%   r3   rl   rC   rm   errorexcofferedr[   r}   s              r   clarify_toolr   I  s~   J 0;;
E 	%e$$$ 	F!N  F!*hHN8K8K8Q8Q8S8STTT F F F!"Ds"D"DEEEEEEEEF  88>>++ 86777~~H '4(( 	DBCCC JICC7CCCIIIw<<+%%l{l+G 	GTUUU
 G"7++>'(G\RR > > ><s<<========>  8+bb7ST`7a7abbb),77:"&  	   s;   1A, ,
B6BBBE- -
F7FFFc                      dS )z>Clarify tool has no external requirements -- always available.TrK   rK   r   r   check_clarify_requirementsr     s    4r   clarifyu  Ask the user a question when you need clarification, feedback, or a decision before proceeding. Supports three modes:

1. **Single-select multiple choice** — provide up to 4 choices. The user picks one or types their own answer via a 5th 'Other' option. List the choice you recommend FIRST: the UI labels it '(Recommended)' and highlights it by default.
2. **Multi-select multiple choice** — set multi_select=true. The user can select multiple options via checkboxes. user_response will be a list of selected choices.
3. **Open-ended** — omit choices entirely. The user types a free-form response.

You can also ask SEVERAL questions in ONE call: pass questions in the `questions` array (each with its own choices/multi_select, any mix of the three modes). The user answers them all on a single form, in any order. STRONGLY preferred over a chain of single-question clarify calls when you need several independent answers.
CRITICAL: when you are offering options, put each option ONLY in the `choices` array — NEVER enumerate the options inside the `question` text. The UI renders `choices` as selectable rows; options written into the question string render as dead prose the user can't pick. Right: question='Which deployment target?', choices=['staging', 'prod']. Wrong: question='Which target? 1) staging 2) prod', choices=[].

Use this tool when:
- The task is ambiguous and you need the user to choose an approach
- You want post-task feedback ('How did that work out?')
- You want to offer to save a skill or update memory
- A decision has meaningful trade-offs the user should weigh in on

Do NOT use this tool for simple yes/no confirmation of dangerous commands (the terminal tool handles that). Prefer making a reasonable default choice yourself when the decision is low-stakes.objectstringu   The question itself, and ONLY the question (e.g. 'Which deployment target?'). Do NOT embed the answer options here — pass them as separate elements in `choices`.)typer   arrayr   u   REQUIRED whenever you are presenting selectable options: each distinct option is its own array element (up to 4). ORDER MATTERS: put the option you actually recommend FIRST — the UI labels it '(Recommended)' and pre-selects it, so a list ordered arbitrarily recommends the wrong thing to the user. Do not write '(Recommended)' yourself. The UI renders these as pickable rows and auto-appends an 'Other (type your answer)' option. Omit this parameter entirely ONLY for a genuinely open-ended free-text question.)r   itemsmaxItemsr   booleanzWhen true, the user can select MULTIPLE options (like checkboxes). The user_response will be a list of selected choices. When false (default), single selection (radio). Has no effect when choices is omitted (open-ended question).u  Ask 2-5 INDEPENDENT questions in one call instead of several sequential clarify calls — the user answers them on one form, in any order. Each item has its own question/choices/multi_select (same rules as the top-level parameters); optional `id` is echoed back in the matching response. When set, the top-level question/choices are ignored. put a short batch title in the top-level `question` The result is {responses: [...]}, with `timed_out: true` added if the user stopped part-way (unanswered entries have an empty user_response). Only batch questions that are truly independent — if one answer would change another question, ask separately.zLOptional short identifier echoed in the matching response (e.g. 'approach').)r   r   r   )rc   rD   r%   r3   )r   
propertiesrequired)r   r   r   r   )rD   r%   r3   rl   )namer   r>   )registryr   c           	          t          |                     dd          |                     d          |                     dd          |                     d          |                    d                    S )	NrD   r   r%   r3   Frl   rC   )rD   r%   r3   rl   rC   )r   r   )argskws     r   <lambda>r   ,  sd    |*b))##XXne44((;''
## %  %  % r   u   ❓)r   toolsetschemahandlercheck_fnemoji)NFNN)!__doc__rW   typingr   r   r   r   ri   rg   r   r+   r   r   r-   r*   rH   r^   r    rq   rk   rt   r   r   ry   r   r   r   r   CLARIFY_SCHEMAtools.registryr   r   registerrK   r   r   <module>r      s     1 1 1 1 1 1 1 1 1 1 1 1
  >  $ #    B@d3i @DI @ @ @ @,C C    ' ' '6<$s) < < < <<;u ; ; ; ;|T    ""t "V " " " "2d4j 24 2D 2S 2 2 2 20/94: /93 /93 /9 /9 /9 /9h $(&*#'` ``d3i ` ` T
#	`
 x ` 	` ` ` `FD     	C<  !G    (+'S " "S   )L % %-!G  &,X$6$+&,h%7(3$ $
 *0(;# #  ",% #% %EH
 H
R  LWL LAm mb 0 / / / / / / /  	% % (
     r   