
    
Bja                     <   U d 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mZm	Z	m
Z
mZ ddlmZmZ ddlmZmZ  ej$                  e      Zda ej,                         Z ej0                         Zd Zd Zd Z e        	 dd	lmZ  e         ejD                         Z#ee$e$f   e%d<    ejL                         Z'ee$e(f   e%d<   g a)e	e$   e%d<   ddgdgdgdgdgg dg ddgg dg ddgdZ*i Z+ee,e	ee$ef      f   e%d<   dAdZ-	 	 	 dBde	e$   de	e$   d e.de	ee$ef      fd!Z/	 	 	 dBde	e$   de	e$   d e.de	ee$ef      fd"Z0h d#Z1d$d%hZ2d&e$d'ee$ef   dee$ef   fd(Z3dCd)e$d*e(dz  fd+Z4d*e(dz  de.fd,Z5d)e$d-e6fd.Z7dDd)e$d/e.fd0Z8d)e$fd1Z9	 	 	 	 	 	 dEd2e$d3ee$ef   d4e
e$   d5e
e$   d6e
e$   d7e
e$   d8e
e	e$      d9e.de$fd:Z:de	e$   fd;Z;d&e$de
e$   fd<Z<dee$e(f   fd=Z=dee$e.f   fd>Z>dDd?e.dee	e$   e	e(   f   fd@Z?y# e$ rZ ejC                  d
e        Y dZ [ dZ [ ww xY w)Fa  
Model Tools Module

Thin orchestration layer over the tool registry. Each tool file in tools/
self-registers its schema, handler, and metadata via tools.registry.register().
This module triggers discovery (by importing all tool modules), then provides
the public API that run_agent.py, cli.py, batch_runner.py, and the RL
environments consume.

Public API (signatures preserved from the original 2,400-line version):
    get_tool_definitions(enabled_toolsets, disabled_toolsets, quiet_mode) -> list
    handle_function_call(function_name, function_args, task_id, user_task) -> str
    TOOL_TO_TOOLSET_MAP: dict          (for batch_runner.py)
    TOOLSET_REQUIREMENTS: dict         (for cli.py, doctor.py)
    get_all_tool_names() -> list
    get_toolset_for_tool(name) -> str
    get_available_toolsets() -> dict
    check_toolset_requirements() -> dict
    check_tool_availability(quiet) -> tuple
    N)DictAnyListOptionalTuple)discover_builtin_toolsregistry)resolve_toolsetvalidate_toolsetc                      t         5  t        t        j                         rt        j                         at        cddd       S # 1 sw Y   yxY w)a^  Return a long-lived event loop for running async tool handlers.

    Using a persistent loop (instead of asyncio.run() which creates and
    *closes* a fresh loop every time) prevents "Event loop is closed"
    errors that occur when cached httpx/AsyncOpenAI clients attempt to
    close their transport on a dead loop during garbage collection.
    N)_tool_loop_lock
_tool_loop	is_closedasyncionew_event_loop     0/home/ubuntu/.hermes/hermes-agent/model_tools.py_get_tool_loopr   -   s>     
 !5!5!7 //1J  s   4AAc                      t        t        dd      } | | j                         r4t        j                         } t        j
                  |        | t        _        | S )u  Return a persistent event loop for the current worker thread.

    Each worker thread (e.g., delegate_task's ThreadPoolExecutor threads)
    gets its own long-lived loop stored in thread-local storage.  This
    prevents the "Event loop is closed" errors that occurred when
    asyncio.run() was used per-call: asyncio.run() creates a loop, runs
    the coroutine, then *closes* the loop — but cached httpx/AsyncOpenAI
    clients remain bound to that now-dead loop and raise RuntimeError
    during garbage collection or subsequent use.

    By keeping the loop alive for the thread's lifetime, cached clients
    stay valid and their cleanup runs on a live loop.
    loopN)getattr_worker_thread_localr   r   r   set_event_loopr   )r   s    r   _get_worker_loopr   <   sK     '6D|t~~'%%'t$$(!Kr   c                 :   	 	 t        j                         }|r|j                         rsddl}d	t        j                          	fd}|j                  j                  d      }|j                  |      }	 |j                  d      |j                  d	       S t        j"                         t        j$                         urt'               		j)                         S t+               }|j)                         S # t        $ r d}Y w xY w# |j                  j                  $ r] j                  d
      rI	G	 t        j                  	      D ]  }	j                  |j                           	  # t        $ r Y  w xY w w xY w# |j                  d	       w xY w)a  Run an async coroutine from a sync context.

    If the current thread already has a running event loop (e.g., inside
    the gateway's async stack or Atropos's event loop), we spin up a
    disposable thread so asyncio.run() can create its own loop without
    conflicting.

    For the common CLI path (no running loop), we use a persistent event
    loop so that cached async clients (httpx / AsyncOpenAI) remain bound
    to a live loop and don't trigger "Event loop is closed" on GC.

    When called from a worker thread (parallel tool execution), we use a
    per-thread persistent loop to avoid both contention with the main
    thread's shared loop AND the "Event loop is closed" errors caused by
    asyncio.run()'s create-and-destroy lifecycle.

    This is the single source of truth for sync->async bridging in tool
    handlers. The RL paths (agent_loop.py, tool_context.py) also provide
    outer thread-pool wrapping as defense-in-depth, but each handler is
    self-protecting via this function.
    Nr   c            	      n   t        j                         j                          	 t        j                         j	                        	 t        j
                        } | D ]  }|j                           | r$j	                  t        j                  | ddi       j                          S # t        $ r Y w xY w# 	 t        j
                        } | D ]  }|j                           | r$j	                  t        j                  | ddi       n# t        $ r Y nw xY wj                          w xY w)Nreturn_exceptionsT)
r   r   setr   run_until_complete	all_taskscancelgather	Exceptionclose)pendingtcoro
loop_readyworker_loops     r   _run_in_workerz"_run_async.<locals>._run_in_workerx   s!   !002KNN$&&{3"55d; &//<G$ #
##66#NNGLtL
 !!# !  &//<G$ #
##66#NNGLtL ! !!#sC   %B? AB00	B<;B<?D4ADD4	D D4D  D4   )max_workersi,  )timeoutF)waitg      ?)r   get_running_loopRuntimeError
is_runningconcurrent.futures	threadingEventfuturesThreadPoolExecutorsubmitresultshutdownTimeoutErrorr/   r!   call_soon_threadsafer"   current_threadmain_threadr   r    r   )
r(   r   
concurrentr+   poolfuturer'   	tool_loopr)   r*   s
   `       @@r   
_run_asyncrC   R   s   ,'') ! 	";?__&
	$, !!444C^,	&===-  MMuM% !)>)>)@@&(--d33 I''--G  P !!.. 
	 s+0G$..{; C#88BC
  $  
	 MMuM%sM   C< :D <D
	D
-F;5E21F2	E>;F=E>>FF F)discover_pluginszPlugin discovery failed: %sTOOL_TO_TOOLSET_MAPTOOLSET_REQUIREMENTS_last_resolved_tool_names
web_searchweb_extractterminalvision_analyzemixture_of_agentsimage_generate)skills_list
skill_viewskill_manage)
browser_navigatebrowser_snapshotbrowser_clickbrowser_typebrowser_scrollbrowser_backbrowser_pressbrowser_get_imagesbrowser_visionbrowser_consolecronjob)
rl_list_environmentsrl_select_environmentrl_get_current_configrl_edit_configrl_start_trainingrl_check_statusrl_stop_trainingrl_get_resultsrl_list_runsrl_test_inference)	read_file
write_filepatchsearch_filestext_to_speech)	web_toolsterminal_toolsvision_tools	moa_toolsimage_toolsskills_toolsbrowser_toolscronjob_toolsrl_tools
file_tools	tts_tools_tool_defs_cachereturnc                  ,    t         j                          y)zDrop memoized get_tool_definitions() results. Called when dynamic
    schema dependencies change (e.g. discord capability cache reset,
    execute_code sandbox reconfigured).N)rv   clearr   r   r   _clear_tool_defs_cacherz     s     r   enabled_toolsetsdisabled_toolsets
quiet_modec                    |r	 ddl m}  |       }|j                         }|j                  |j                  f}| t        |       nd|rt        |      ndt        j                  |f}t        j                  |      }|!|D 	cg c]
  }	|	d   d    c}	at        |      S t        | ||      }
|r|
t        <   t        |
      S |
S # t
        t        t        f$ r d}Y w xY wc c}	w )a  
    Get tool definitions for model API calls with toolset-based filtering.

    All tools must be part of a toolset to be accessible.

    Args:
        enabled_toolsets: Only include tools from these toolsets.
        disabled_toolsets: Exclude tools from these toolsets (if enabled_toolsets is None).
        quiet_mode: Suppress status prints.

    Returns:
        Filtered list of OpenAI-format tool definitions.
    r   )get_config_pathNfunctionname)hermes_cli.configr   statst_mtime_nsst_sizeFileNotFoundErrorOSErrorImportError	frozensetr	   _generationrv   getrG   list_compute_tool_definitions)r{   r|   r}   r   cfg_pathcfg_statcfg_fp	cache_keycachedr'   r9   s              r   get_tool_definitionsr     s    4 	9&(H}}H**H,<,<=F ,<+GI&'T,=I'(4  	
	 "%%i0 IO(O1:v)>(O% <&'79JJWF '-#F|M; "7K8 	F	 )Ps   5C C CCc           	      B   t               }| | D ]  }t        |      rDt        |      }|j                  |       |r-t	        d| d|rdj                  |      nd        R|t        v r>t        |   }|j                  |       |rwt	        d| ddj                  |              |rt	        d|         n,dd	lm}  |       D ]  }|j                  t        |              |r|D ]  }t        |      rDt        |      }|j                  |       |r-t	        d
| d|rdj                  |      nd        R|t        v r>t        |   }|j                  |       |rwt	        d| ddj                  |              |rt	        d|         t        j                  ||      }	|	D 
ch c]
  }
|
d   d    }}
d|v r_ddlm}m}m} ||z  } || |             }t!        |	      D ]3  \  }}|j#                  di       j#                  d      dk(  s+d|d|	|<    n ddd}|D ]  }||v s	 ddlm} t)        |||         } |       }|E|	D 
cg c](  }
|
j#                  di       j#                  d      |k7  r|
* }	}
|j-                  |       lt!        |	      D ]3  \  }}|j#                  di       j#                  d      |k(  s+d|d|	|<      d|v rzddh|z  }|sqt!        |	      D ]c  \  }}|j#                  di       j#                  d      dk(  s+|d   j#                  dd      }|j/                  dd      }di |d   d|id|	|<    n |sL|	r?|	D 
cg c]
  }
|
d   d    }}
t	        dt1        |	       ddj                  |              nt	        d       |	D 
cg c]
  }
|
d   d    c}
a	 dd lm}  ||	      }	|	S c c}
w # t*        $ r d}Y w xY wc c}
w c c}
w c c}
w # t*        $ r!}t8        j;                  d!|       Y d}~|	S d}~ww xY w)"z8Uncached implementation of :func:`get_tool_definitions`.Nu   ✅ Enabled toolset 'z': z, zno toolsu   ✅ Enabled legacy toolset 'u   ⚠️  Unknown toolset: r   )get_all_toolsetsu   🚫 Disabled toolset 'u   🚫 Disabled legacy toolset 'quietr   r   execute_code)SANDBOX_ALLOWED_TOOLSbuild_execute_code_schema_get_execution_mode)mode)typer   get_dynamic_schema_coreget_dynamic_schema_admin)discorddiscord_admin)discord_toolrQ   rH   rI   description zV For simple information retrieval, prefer web_search or web_extract (faster, cheaper).u   🛠️  Final tool selection (z	 tools): u<   🛠️  No tools selected (all filtered out or unavailable))sanitize_tool_schemaszSchema sanitization skipped: %s)r   r   r
   updateprintjoin_LEGACY_TOOLSET_MAPtoolsetsr   difference_updater	   get_definitionstools.code_execution_toolr   r   r   	enumerater   toolsr   r   r$   discardreplacelenrG   tools.schema_sanitizerr   loggerwarning)r{   r|   r}   tools_to_includetoolset_nameresolvedlegacy_toolsr   ts_namefiltered_toolsr'   available_tool_namesr   r   r   sandbox_enableddynamic_schemaitd_discord_schema_fnsdiscord_tool_name_dt	schema_fndynamicweb_tools_availabledesc
tool_namesr   es                                r   r   r   O  s     E#, 	BL-*<8 ''1!1,sZb499XCVhrBstu!442<@ ''5!8c$))T`JaIbcd1,@A	B 	.') 	>G##OG$<=	> - 	BL-*<8 228<!3L>\dTYYxEXjtDuvw!442<@ 22<@!:<.DIIVbLcKdef1,@A	B* --.>jQN <JJaAjM&1JJ --ss/2FF2?I\I^_~. 	EArvvj"%))&1^C-7^$Tq!	 -3 1  445#C)<=N)OP	#+ -"uuZ,008<MM " " %,,->?&~6 EArvvj"-11&9=NN5?W,Uq)0 11+];>RR"">2 266*b)--f59KKj>--mR@D<<pD
 !+$Kr*~$K}d$K)N1%  9GHA!J-/HJH3C4G3H	RVR[R[\fRgQhijPQ AO O1:v!6 O=@.~> m K<  "B I !P  =8!<<=sB   1O5O-O%!O*+O/>O4 O"!O"4	P=PP>   todomemorydelegate_tasksession_searchrf   ri   	tool_nameargsc                 J   |rt        |t              s|S t        j                  |       }|s|S |j	                  d      xs i j	                  d      }|s|S t        |j                               D ]0  \  }}|j	                  |      }|s|j	                  d      }|dk(  r|t        |t
        t        f      st        |t              rlt        |||      }||ur|||<   q|j                         j                  d      rt        j                  d| |       |g||<   t        j                  d| |       |g||<   t        j                  d	t        |      j                   | |       t        |t              s	|st#        |      st        |||      }||us,|||<   3 |S )
a  Coerce tool call arguments to match their JSON Schema types.

    LLMs frequently return numbers as strings (``"42"`` instead of ``42``)
    and booleans as strings (``"true"`` instead of ``true``).  This compares
    each argument value against the tool's registered JSON Schema and attempts
    safe coercion when the value is a string but the schema expects a different
    type.  Original values are preserved when coercion fails.

    Handles ``"type": "integer"``, ``"type": "number"``, ``"type": "boolean"``,
    and union types (``"type": ["integer", "string"]``).

    Also wraps bare scalar values in a single-element list when the schema
    declares ``"type": "array"``.  Open-weight models (DeepSeek, Qwen, GLM)
    sometimes emit ``{"urls": "https://a.com"}`` when the tool expects
    ``{"urls": ["https://a.com"]}``; wrapping here avoids a confusing tool
    failure on what is otherwise a well-formed call.
    
parameters
propertiesr   arrayschema[u   coerce_tool_args: %s.%s looks like a JSON array string but could not be parsed — model may have emitted a JSON-encoded string instead of a native array. Falling back to single-element list.z7coerce_tool_args: wrapped bare string in list for %s.%sz3coerce_tool_args: wrapped bare %s in list for %s.%s)
isinstancedictr	   
get_schemar   r   itemstuplestr_coerce_valuestrip
startswithr   r   infor   __name___schema_allows_null)	r   r   r   r   keyvalueprop_schemaexpectedcoerceds	            r   coerce_tool_argsr     s   $ z$-  +F**\*0b55lCJ4::<( 2 
U nnS)??6* w5#4ZPTV[}=]%%'xL%' !(DI ;;=++C0NN? "3 #GS	Ms DIKKEU$$i %% 3K @xD%DIe2 h Kr   r   r   c                    t        |      r"| j                         j                         dk(  ryt        |t              r|D ]  }t        | ||      }|| us|c S  | S |dv rt        | |dk(        S |dk(  rt        |       S |dk(  rt        | t              S |d	k(  rt        | t              S |dk(  r"| j                         j                         dk(  ry| S )
zAttempt to coerce a string *value* to *expected_type*.

    Returns the original string when coercion is not applicable or fails.
    nullNr   >   numberintegerr   )integer_onlybooleanr   object)
r   r   lowerr   r   r   _coerce_number_coerce_boolean_coerce_jsonr   )r   expected_typer   r'   r9   s        r   r   r   I  s    
 6"u{{}':':'<'F-& 	A"5!F;FU"	 --e=I3MOO	!u%%E4(( E4((5;;=#6#6#8F#BLr   c                 V   t        | t              sy| j                  d      }|dk(  ryt        |t              rd|v ry| j                  d      du rydD ]S  }| j                  |      }t        |t              s%|D ]*  }t        |t              s|j                  d      dk(  s)  y U y)z@Return True when a JSON Schema fragment explicitly permits null.Fr   r   Tnullable)anyOfoneOf)r   r   r   r   )r   schema_type	union_keyvariantsvariants        r   r   r   f  s    fd#**V$Kf+t$;)>zz*%' 	::i((D) 	G'4(W[[-@F-J		 r   expected_python_typec                 t   	 t        j                  |       }t        ||      r"t        j                  d|j                         |S t        j                  dt        |      j                  |j                         | S # t        t        f$ r-}t        j                  d|j                  |       | cY d}~S d}~ww xY w)aJ  Parse *value* as JSON when the schema expects an array or object.

    Handles model output drift where a complex oneOf/discriminated-union schema
    causes the LLM to emit the array/object as a JSON string instead of a native
    structure.  Returns the original string if parsing fails or yields the wrong
    Python type.
    zIcoerce_tool_args: failed to parse string as JSON for expected type %s: %sNz5coerce_tool_args: coerced string to %s via json.loadsuL   coerce_tool_args: JSON-parsed value is %s, expected %s — skipping coercion)
jsonloads
ValueError	TypeErrorr   r   r   r   debugr   )r   r   parsedexcs       r   r   r   ~  s    E" &./C ))	
 
NNVV%%
 L% 	" W ))	

 s   A; ;B7
"B2,B72B7r   c                     	 t        |       }||k7  s|t        d      k(  s|t        d      k(  r| S |t        |      k(  rt        |      S |r| S |S # t        t        f$ r | cY S w xY w)zFTry to parse *value* as a number.  Returns original string on failure.infz-inf)floatr  OverflowErrorint)r   r   fs      r   r   r     sp    %L 	AveEl"a5=&8CF{1vH & s   A A#"A#c                 Z    | j                         j                         }|dk(  ry|dk(  ry| S )zGTry to parse *value* as a boolean.  Returns original string on failure.trueTfalseF)r   r   )r   lows     r   r   r     s/    
++-


C
f}
g~Lr   function_namefunction_argstask_idtool_call_id
session_id	user_taskenabled_toolsskip_pre_tool_call_hookc           
         t        | |      }	 | t        v rt        j                  d|  di      S |s=d}	 ddlm}	  |	| ||xs d|xs d|xs d      }|t        j                  d|id	
      S | t        vr	 ddl
m}  ||xs d       t        j                         }| dk(  r$||nt        }t        j                   | |||      }nt        j                   | |||      }t#        t        j                         |z
  dz        }	 ddlm}  |d| |||xs d|xs d|xs d|       	 ddlm}  |d| |||xs d|xs d|xs d|      }|D ]  }t'        |t(              s|} |S  	 |S # t        $ r!}
t        j                  d|
       Y d}
~
9d}
~
ww xY w# t        $ r Y w xY w# t        $ r }
t        j                  d|
       Y d}
~
d}
~
ww xY w# t        $ r!}
t        j                  d|
       Y d}
~
|S d}
~
ww xY w# t        $ rI}d|  dt)        |       }t        j+                  |       t        j                  d|id	
      cY d}~S d}~ww xY w)a  
    Main function call dispatcher that routes calls to the tool registry.

    Args:
        function_name: Name of the function to call.
        function_args: Arguments for the function.
        task_id: Unique identifier for terminal/browser session isolation.
        user_task: The user's original task (for browser_snapshot context).
        enabled_tools: Tool names enabled for this session.  When provided,
                       execute_code uses this list to determine which sandbox
                       tools to generate.  Falls back to the process-global
                       ``_last_resolved_tool_names`` for backward compat.

    Returns:
        Function result as a JSON string.
    errorz" must be handled by the agent loopNr   )get_pre_tool_call_block_messager   )r  r  r  zpre_tool_call hook error: %sF)ensure_ascii)notify_other_tool_calldefaultr   )r  r  )r  r  i  )invoke_hookpost_tool_call)r   r   r9   r  r  r  duration_mszpost_tool_call hook error: %stransform_tool_resultz$transform_tool_result hook error: %szError executing z: )r   _AGENT_LOOP_TOOLSr   dumpshermes_cli.pluginsr  r$   r   r  _READ_SEARCH_TOOLStools.file_toolsr  time	monotonicrG   r	   dispatchr
  r  r   r   	exception)r  r  r  r  r  r  r  r  block_messager  	_hook_errr  _dispatch_startr   r9   r   r  hook_resultshook_resultr   	error_msgs                        r   handle_function_callr1    s   6 %]MBMnD--::w=/9[(\]^^ '+/M
HN ?!!#Mr)/R!-!3! (zz7M":OO  22C&w';)< ..*N* 0=/HmNgO&&}-F &&}#F
 4>>+o=EF	E6 '"2%+)/R'	&	L6&''"2%+)/R'	L  , k3/(F  a  H;YGGH  P  	ELL8)DD	E2  	LLL?KK	L
  D&}oRAx@	#zz7I.UCCDs   !G> G> E( G> /G> 8F 
A9G> "F% '7G G "G> $G &G> (	F1FG> FG> 	F"G> !F""G> %	G.G	G> 	GG> 	G;G60G> 6G;;G> >	I>IIIc                  *    t        j                         S )z!Return all registered tool names.)r	   get_all_tool_namesr   r   r   r3  r3  K  s    &&((r   c                 ,    t        j                  |       S )z%Return the toolset a tool belongs to.)r	   get_toolset_for_tool)r   s    r   r5  r5  P  s    ((33r   c                  *    t        j                         S )z0Return toolset availability info for UI display.)r	   get_available_toolsetsr   r   r   r7  r7  U  s    **,,r   c                  *    t        j                         S )z>Return {toolset: available_bool} for every registered toolset.)r	   check_toolset_requirementsr   r   r   r9  r9  Z  s    ..00r   r   c                 .    t        j                  |       S )z.Return (available_toolsets, unavailable_info).r   )r	   check_tool_availabilityr   s    r   r;  r;  _  s    ++%88r   )rw   N)NNF)N)F)NNNNNF)@__doc__r   r   loggingr4   r'  typingr   r   r   r   r   tools.registryr   r	   r   r
   r   	getLoggerr   r   r   Lockr   localr   r   r   rC   r$  rD   r$   r   r  get_tool_to_toolset_maprE   r   __annotations__get_toolset_requirementsrF   r   rG   r   rv   r   rz   boolr   r   r"  r%  r   r   r   r   r   r   r   r1  r3  r5  r7  r9  r;  r   r   r   <module>rG     s  *      3 3 ; 6			8	$ 
 ).."&y( ,[.D   33 'Gh&F&F&H T#s(^ H(I(I(I(K d39o K (* 49 ) .!l%&%&$%A  [ G"#- R 79 $ud4S>223 8 #'#'=3i=Cy= = 
$sCx.	=B #'#'S3iSCyS S 
$sCx.	S| J !>2 Q Q4S> Qd38n Qh TD[ :t  0 4 ># T $3  ""& $#)-$)KDKDS>KD c]KD 3-	KD
 KD }KD DI&KD "KD 	KDd)DI )
4C 4HSM 4
-S$Y -
1DdO 1
94 9E$s)T$Z:O4P 9q  3
LL.223s   =G9 9H>HH