code: def observe(self, reward=0, terminal=False, **kwargs):
      if self.current_state is None:
          return None
  
      if self.callbacks.get("before_observe") is not None:
          self.callbacks["before_observe"]()
  
      rewards = torch.from_numpy(
          np.expand_dims(np.stack([reward]), 1)
      ).float()
      masks = torch.tensor(
          [0.0] if terminal else [1.0], dtype=torch.float32
      )
  
      self.current_state *= masks[0]
  
      self.storage.insert(
          self.current_state,
          self.current_states,
          self.current_action,
          self.current_action_log_prob,
          self.current_value,
          rewards,
          masks,
      )
  
      if terminal:
          self.current_episode += 1
  
      self.current_step += 1
  
      if self.storage.step == (self.storage.num_steps - 1):
          if self.callbacks.get("before_update") is not None:
              self.callbacks["before_update"]()
  
          with torch.no_grad():
              next_value = self.actor_critic.get_value(
                  self.storage.observations[-1],
                  self.storage.states[-1],
                  self.storage.masks[-1],
              ).detach()
  
          self.storage.compute_returns(
              next_value, self.gae, self.discount, self.gae_tau
          )
  
          value_loss, action_loss, entropy = self.agent.update(
              self.storage
          )
          self.analytics_client.track(
              event_key="PPO_INTERNALS",
              data={
                  "value_loss": value_loss,
                  "action_loss": action_loss,
                  "entropy": entropy,
              },
          )
  
          self.logger.log_metric(
              "entropy", entropy, step=self.current_step
          )
          self.logger.log_metric(
              "value_loss", value_loss, step=self.current_step
          )
          self.logger.log_metric(
              "action_loss", action_loss, step=self.current_step
          )
  
          self.storage.after_update()
  
          if self.callbacks.get("after_update") is not None:
              self.callbacks["after_update"]()
  
      if self.current_step % self.save_steps == 0:
          if self.callbacks.get("before_update") is not None:
              self.callbacks["before_update"]()
  
          self.save_model()
  
          if self.callbacks.get("after_update") is not None:
              self.callbacks["after_update"]()
  
      self.current_state = None
  
      self.current_reward = reward
      self.cumulative_reward += reward
  
      self.analytics_client.track(
          event_key="REWARD",
          data={
              "reward": self.current_reward,
              "total_reward": self.cumulative_reward,
          },
      )
  
      if terminal:
          self.analytics_client.track(
              event_key="TOTAL_REWARD",
              data={"reward": self.cumulative_reward},
          )
          self.logger.log_metric(
              "episode_rewards",
              self.cumulative_reward,
              step=self.current_step,
          )
  
      if self.callbacks.get("after_observe") is not None:
          self.callbacks["after_observe"]()
  
file_path: SerpentAI/serpent/machine_learning/reinforcement_learning/agents/ppo_agent.py
imports: from serpent.machine_learning.reinforcement_learning.agent import Agent
  from serpent.game_frame import GameFrame
  from serpent.game_frame_buffer import GameFrameBuffer
  from serpent.enums import InputControlTypes
  from serpent.logger import Loggers
  from serpent.utilities import SerpentError
  import os
  import enum
  import copy
  import json
  import numpy
label_desc: Use image node_id 3 for calling the PPOAgent obj's underlying member method code with example usage: obj.observe(reward, terminal) and returns: None