class_docstr: Base world for running TOD model-model chats. Includes the following agents:
  
  * User utt agent
  * API call agent
      * Currently assumed to be same as system utt agent in script code, though used as if separate in this world for clarity
  * API responder agent
  * System utt agent
  * API schema groundinger agent (given to api call + response agent)
  * Goal groundinger agent (given to user)
  
  As is standard for ParlAI, these agents may be models or may be standalone classes that extend the "Agent" class. The models for these *are* expected to have their utterances in a standard format.
  
  We do expect these agents to be passed in with a set order (see above), since some assumptions of regular ParlAI Worlds (ex. task = agent[0], model = agent[1]) are broken here since there is no "task agent" and one agent can be two "roles" (ex. system agent also making API calls)
code: def _observe_and_act(
      self,
      observe_idx,
      act_idx,
      info="for regular parley",
      override_act_idx=None,
  ):
      act_agent_idx = override_act_idx if override_act_idx else act_idx
      act_agent = self.agents[act_agent_idx]
      record_output_idx = act_idx
      if hasattr(act_agent, "batch_act"):
          batch_observations = []
          for i in range(self.batchsize):
              if not self.end_episode[i]:
                  observe = self.batch_acts[i][observe_idx]
                  observe = self.batch_agents[i][act_agent_idx].observe(
                      observe
                  )
                  batch_observations.append(Message(observe))
              else:
                  # We're done with this episode, so just do a pad.
                  # NOTE: This could cause issues with RL down the line
                  batch_observations.append(Message.padding_example())
                  self.batch_acts[i][record_output_idx] = {
                      "text": "",
                      "id": "",
                  }
          batch_actions = act_agent.batch_act(batch_observations)
          for i in range(self.batchsize):
              if self.end_episode[i]:
                  continue
              self.batch_acts[i][record_output_idx] = batch_actions[i]
              self.batch_agents[i][record_output_idx].self_observe(
                  batch_actions[i]
              )
      else:  # Run on agents individually
          for i in range(self.batchsize):
              act_agent = (
                  self.batch_agents[i][override_act_idx]
                  if override_act_idx
                  else self.batch_agents[i][act_idx]
              )
              if (
                  hasattr(act_agent, "episode_done")
                  and act_agent.episode_done()
              ):
                  self.end_episode[i] = True
              if self.end_episode[i]:
                  # Following line exists because:
                  # 1. Code for writing converseations is not hapy if an "id" does not exists with a sample
                  # 2. Because of the `self.end_episode` code, no agent will see this example anyway.
                  self.batch_acts[i][record_output_idx] = {
                      "text": "",
                      "id": "",
                  }
                  continue
              act_agent.observe(self.batch_acts[i][observe_idx])
              if isinstance(act_agent, LocalHumanAgent):
                  print(
                      f"Getting message for {SPEAKER_TO_NAME[record_output_idx]} for {info} in batch {i}"
                  )
              try:
                  self.batch_acts[i][
                      record_output_idx
                  ] = act_agent.act()
              except StopIteration:
                  self.end_episode[i] = True
      for i in range(self.batchsize):
          if self.end_episode[i]:
              continue
          self.batch_tod_world_metrics[i].handle_message(
              self.batch_acts[i][record_output_idx],
              SPEAKER_TO_NAME[act_agent_idx],
          )
          if tod.STANDARD_DONE in self.batch_acts[i][
              record_output_idx
          ].get("text", ""):
              # User models trained to output a "DONE" on last turn; same with human agents.
              self.end_episode[i] = True
  
imports: from parlai.core.metrics import Metric, LegacyMetric
  from parlai.core.message import Message
  from parlai.core.opt import Opt
  from parlai.core.worlds import World
  from parlai.agents.local_human.local_human import LocalHumanAgent
  from parlai.utils.misc import display_messages
  import parlai.core.tod.tod_core
  import parlai.core.tod.world_metrics
  import sys
  import copy
label_desc: Use image node_id 4 for calling the TodWorld obj's underlying member method code with example usage: obj._observe_and_act(observe_idx, act_idx, info, override_act_idx) without return types