When you want to build loops in MainlyAI you use iterator fields. Fields are defined by a field Transmitter and a field Receiver. Any nodes between these will be part of the Field and will be iterated until either the Transmitter runs out of data or the Receiver decides its conditions are met.

To mark a Transmitter or Reciever as a feild, set is_field=True on the attribute decorator.

@wob.transmitter("value", "name", is_field=True)
def transmit_values(self):
  ...
@wob.receiver("value", "name", is_field=True)
def receiver_values(self):
  ...

A field Transmitter is expected to return a tuple. The first element is a error handler and it can be None. The second element is a regular python iterator.

Here’s an example of a node that splits a string by commas and returns the strings as a Field.


@wob.init()
def init(self):
  self.values = ['a', 'b', 'c']
  self.iterator = None

@wob.receiver("value","input")
def receive_input(self, i):
  self.values = i.split(',')

@wob.transmitter("value", "output", is_field=True)
def transmit_values(self):
  return self.iterator

@wob.execute()
def execute(self):
  self.iterator = (None, iter(self.values))

This pattern will run the nodes A and B for every a,b,c.

Writing a field Receiver is similar to writing a normal Receiver, it will just be called multiple times until the Transmitter runs out of data. Here’s an example of a node that prints each value in a Field.

@wob.receiver("value", "input", is_field=True)
def receive_values(self, i):
	print(i)

After all values have been received, execution will continue like normal. However, the Reciever can also use one of the Execution Context hooks to break out of the loop early. Here’s an example of a node that will only recieve the first two values in a Field, then break back into normal execution.

@wob.receiver("value", "input", is_field=True)
def receive_values(self, i):
	print(i)
	self.count_received += 1
  self.aggr.append(i) # if you want to aggregate the things you iterate over.

Error handlers

A field can use an error handler for controlling how errors are handled in the field.

Example:

from mirmod import miranda

@wob.init()
def init(self):
  self.value = None
  self.itr = None

@wob.receiver("value","input")
def receive_value(self, value):
  self.value = value

@wob.transmitter("value", "output", is_field=True)
def transmit_value(self):
  return self.itr

@wob.execute()
async def execute(self):
  class MyExceptionHandler:
    async def __call__(self, execution_context, executeNodeFunc, wob):
      try:
        await executeNodeFunc(wob)
      except Exception as e:
        print ("Cool!")
        print (e)
        return miranda.F_NEXT_ELEMENT
      finally:
        pass
      return miranda.F_PROCEED
    
  self.itr = (MyExceptionHandler(), iter([1,2,3,4,5]))

MyExceptionHandler will trap any error, write “Cool!” to the log and then continue to the next element in the iterator.

The following behvaiors are currently supported:

Return status codeDescription
F_NEXT_ELEMENTContinue with the next element on the iterator
F_PROCEEDContinue with the next node in the field (normal behavior)
F_EXITExhaust the iterator and move to the receiver node execution
F_TRY_AGAINRepeat execution of the node that raised the exception
F_RESTARTReset and restart the iterator from the beginning and try again

Advanced setup

Some rules apply for all iterator fields:

  • Inbound edges connected to nodes on a different branch are only executed once before the field transmitter. These nodes are called initialization nodes.
  • Outbound edges from inside the field to a leaf node are repeatedly executed on each iteration.
  • Don’t connect outbound edges to nodes outside of the field. Instead collect the result of the field operation in the field receiver and connect your out-edges on this node.
  • Fields can be nested.
  • If there are multiple field transmitters on a node, the node is a dispatch node and it follows different rules.
  • For regular fields only use one field transmitter and one field receiver.