@step async def handle_tool_calls( self, ctx: Context, ev: ToolCallEvent ) -> PrepEvent: """ Appeller les outils en toute sécurité en gérant les erreurs et en ajoutant leurs résultats au raisonnement en cours. Ensuite, en émettant un PrepEvent, effectuer une nouvelle itération d'invite et d'analyse ReAct. """ tool_calls = ev.tool_calls tools_by_name = {tool.metadata.get_name(): tool for tool in self.tools} current_reasoning = await ctx.store.get( "current_reasoning", default=[] ) sources = await ctx.store.get("sources", default=[]) ''' validation anticipée via introspection Pydantic On intercepte chaque appel, et pour chaque outil : 1. Identifier sa classe d’entrée Pydantic. 2. Instancier cette classe avec les tool_kwargs. 3. Si la validation échoue → bloquer l’appel et injecter une observation. 4. Sinon exécuter normalement. ''' for tool_call in tool_calls: tool_name = tool_call.tool_name tool = tools_by_name.get(tool_name) if not tool: current_reasoning.append( ObservationReasoningStep(observation=f"Tool {tool_name} does not exist") ) continue # Étape 1 : récupérer la classe Pydantic d'entrée input_model = getattr(tool.metadata, "input_model", None) if input_model: try: # Étape 2 : valider les kwargs via Pydantic input_model(**tool_call.tool_kwargs) except ValidationError as ve: # Étape 3 : bloquer l’appel et injecter l’erreur current_reasoning.append( ObservationReasoningStep( observation=( f"Appel bloqué : les paramètres fournis pour l’outil '{tool_name}' " f"sont invalides selon sa définition. Détail : {ve.errors()}" ) ) ) continue # Étape 4 : poursuivre si tout est valide try: tool_output = tool(**tool_call.tool_kwargs) sources.append(tool_output) current_reasoning.append( ObservationReasoningStep(observation=tool_output.content) ) except Exception as e: current_reasoning.append( ObservationReasoningStep( observation=f"Error calling tool {tool.metadata.get_name()}: {e}" ) )