class_docstr: Epsilon-greedy Exploration class that produces exploration actions.
  
  When given a Model's output and a current epsilon value (based on some
  Schedule), it produces a random action (if rand(1) < eps) or
  uses the model-computed one (if rand(1) >= eps).
docstr: TF method to produce the tf op for an epsilon exploration action.
  
  Args:
      action_distribution: The instantiated ActionDistribution object
          to work with when creating exploration actions.
  
  Returns:
      The tf exploration-action op.
docstr_tok: ["TF","method","to","produce","the","tf","op","for","an","epsilon","exploration","action",".","Args",":","action_distribution",":","The","instantiated","ActionDistribution","object","to","work","with","when","creating","exploration","actions",".","Returns",":","The","tf","exploration-action","op","."]
code: def _get_tf_exploration_action_op(
      self,
      action_distribution: ActionDistribution,
      explore: Union[bool, TensorType],
      timestep: Union[int, TensorType],
  ) -> "tf.Tensor":
      """TF method to produce the tf op for an epsilon exploration action.
  
      Args:
          action_distribution: The instantiated ActionDistribution object
              to work with when creating exploration actions.
  
      Returns:
          The tf exploration-action op.
      """
      # TODO: Support MultiActionDistr for tf.
      q_values = action_distribution.inputs
      epsilon = self.epsilon_schedule(
          timestep if timestep is not None else self.last_timestep
      )
  
      # Get the exploit action as the one with the highest logit value.
      exploit_action = tf.argmax(q_values, axis=1)
  
      batch_size = tf.shape(q_values)[0]
      # Mask out actions with q-value=-inf so that we don't even consider
      # them for exploration.
      random_valid_action_logits = tf.where(
          tf.equal(q_values, tf.float32.min),
          tf.ones_like(q_values) * tf.float32.min,
          tf.ones_like(q_values),
      )
      random_actions = tf.squeeze(
          tf.random.categorical(random_valid_action_logits, 1), axis=1
      )
  
      chose_random = (
          tf.random.uniform(
              tf.stack([batch_size]),
              minval=0,
              maxval=1,
              dtype=tf.float32,
          )
          < epsilon
      )
  
      action = tf.cond(
          pred=tf.constant(explore, dtype=tf.bool)
          if isinstance(explore, bool)
          else explore,
          true_fn=(
              lambda: tf.where(
                  chose_random, random_actions, exploit_action
              )
          ),
          false_fn=lambda: exploit_action,
      )
  
      if (
          self.framework == "tf2"
          and not self.policy_config["eager_tracing"]
      ):
          self.last_timestep = timestep
          return action, tf.zeros_like(action, dtype=tf.float32)
      else:
          assign_op = tf1.assign(
              self.last_timestep, tf.cast(timestep, tf.int64)
          )
          with tf1.control_dependencies([assign_op]):
              return action, tf.zeros_like(action, dtype=tf.float32)
  
imports: import gymnasium
  import numpy
  import tree
  import random
  from typing import Union, Optional
  from ray.rllib.models.torch.torch_action_dist import TorchMultiActionDistribution
  from ray.rllib.models.action_dist import ActionDistribution
  from ray.rllib.utils.annotations import override, PublicAPI
  from ray.rllib.utils.exploration.exploration import Exploration, TensorType
  from ray.rllib.utils.framework import try_import_tf, try_import_torch, get_variable
  from ray.rllib.utils.from_config import from_config
  from ray.rllib.utils.numpy import convert_to_numpy
  from ray.rllib.utils.schedules import Schedule, PiecewiseSchedule
  from ray.rllib.utils.torch_utils import FLOAT_MIN
label_desc: Use image node_id 3 for calling the EpsilonGreedy obj's underlying member method code with example usage: obj._get_tf_exploration_action_op(action_distribution, explore, timestep) and returns: action,  tf, action,  tf