Tutorial: Communication

LightJason architecture does not support in general a built-in communication, because communication and agent addressing / naming depends on the domain or underlying software architecture.

To create a communication structure you have to build-up your own naming model, a send action with a receiving plan and a data structure to map agent names / addresses to agent objects.

Contents [Hide]

Previous Knowledge

The tutorial can be done in two steps:

  1. the basic agent definition from the AgentSpeak 15min tutorial
  2. the triggering and action tutorial to create a send-action

Don’t reinvent the edge
Communication can be a very expensive calling structure, especially on distributed systems. If you build your own communication structure just think about multi-threading and performance aspects. Within this tutorial we cannot show you all details of fast and efficient communication data structures, so we would like to show you the basics only. In a distributed system, you have to organise the naming schema and searching methods of names and objects. If you need to transfer messages over the network, just think about serialisation and deserialisation performance. Java supports a serialize interface so don’t create self-defined string data structure, because for such message transfering there are a lot of other and well-known and estabilished components. Well known formats are JSON, YAML or XML/XSD with Jaxb

Agent

For this example we create a small agent, which sends a random message to the agent with the name agent 0. The initial-goal triggers the main-plan, which generates the message and calls the send action.


!main.
+!main <-
    
    R = string/random( "abcdefghijklmnopqrstuvwxyz", 12 );
    
    
     message/send("agent 0", R)
.
+!message/receive(  message( Message ), from( AgentName )  ) <-
    
    generic/print( MyName, " received message [", Message, "] from [", AgentName, "]")
.

Agent with name

For communication a name resolution is needed, so the agents needs to get a name (here a string). This name will be used to determine the sender of a message


package myagentproject;
import org.lightjason.agentspeak.agent.IBaseAgent;
import org.lightjason.agentspeak.configuration.IAgentConfiguration;
import javax.annotation.Nonnull;
final class MyCommunicationAgent extends IBaseAgent<MyCommunicationAgent>
{
    
    private static final long serialVersionUID = 3523915760001772665L;
    
    private final String m_name;
    
    MyCommunicationAgent( @Nonnull final IAgentConfiguration<MyCommunicationAgent> p_configuration, @Nonnull final String p_name )
    {
        super( p_configuration );
        m_name = p_name;
    }
    
    @Nonnull
    final String name()
    {
        return m_name;
    }
}

Agent factory with name generating

The agent factory must create the agent object and a unique name. Within this example we use one factory only, so each factory creates a send action and the send action contains the name resolution. Based on this, the action must be accessible within the factory to register each agent. The name definition is here with the schema agent <number> but keep in mind that the generate method can be called in parallel, so the counter must be thread-safe. Java supports such atomic variables.


package myagentproject;
import org.lightjason.agentspeak.common.CCommon;
import org.lightjason.agentspeak.generator.IBaseAgentGenerator;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import java.util.stream.Stream;
final class MyAgentGenerator extends IBaseAgentGenerator<MyCommunicationAgent>
{
    
    private final CSend m_send;
    
    private final AtomicLong m_counter = new AtomicLong();
    
    MyAgentGenerator( @Nonnull final InputStream p_stream, @Nonnull final CSend p_send ) throws Exception
    {
        super(
            
            p_stream,
            
            Stream.concat(
                
                CCommon.actionsFromPackage(),
                
                Stream.of( p_send )
                
            ).collect( Collectors.toSet() ),
            
            new CVariableBuilder()
        );
        m_send = p_send;
    }
    
    @Nullable
    @Override
    public final MyCommunicationAgent generatesingle( @Nullable final Object... p_data )
    {
        
        
        return m_send.register(
            new MyCommunicationAgent(
                
                
                
                m_configuration, MessageFormat.format( "agent {0}", m_counter.getAndIncrement() )
                
            )
        );
    }
    
    final void unregister( final MyCommunicationAgent p_agent )
    {
        m_send.unregister( p_agent );
    }
}

Send-Action with address resolution

For communication basics a send action must be created. This actions needs also an address resolution for the agent names, this can be an URL access or a string name. Within this example we use a map with string for the agent name and the value for the agent object. Each generated agent must be registered at this action so that other agents can send messages. The action tries to find the agent object based on the name, builds the goal-trigger and transfers the data to the other agent. In the next cycle call of the receiving agent, the message goal-plan will be triggered.


package myagentproject;
import org.lightjason.agentspeak.action.IBaseAction;
import org.lightjason.agentspeak.agent.IAgent;
import org.lightjason.agentspeak.common.CPath;
import org.lightjason.agentspeak.common.IPath;
import org.lightjason.agentspeak.language.CLiteral;
import org.lightjason.agentspeak.language.CRawTerm;
import org.lightjason.agentspeak.language.ITerm;
import org.lightjason.agentspeak.language.execution.IContext;
import org.lightjason.agentspeak.language.fuzzy.CFuzzyValue;
import org.lightjason.agentspeak.language.fuzzy.IFuzzyValue;
import org.lightjason.agentspeak.language.instantiable.plan.trigger.CTrigger;
import org.lightjason.agentspeak.language.instantiable.plan.trigger.ITrigger;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
final class CSend extends IBaseAction
{
    
    private static final long serialVersionUID = 2043960703654019947L;
    
    private final Map<String, MyCommunicationAgent> m_agents = new ConcurrentHashMap<>();
    @Nonnull
    @Override
    public final IPath name()
    {
        return CPath.from( "message/send" );
    }
    @Override
    public final int minimalArgumentNumber()
    {
        return 2;
    }
    @Override
    public final IFuzzyValue<Boolean> execute( final boolean p_parallel, @Nonnull final IContext p_context,
                                               @Nonnull final List<ITerm> p_argument, @Nonnull final List<ITerm> p_return )
    {
        
        final IAgent<?> l_receiver = m_agents.get( p_argument.get( 0 ).<String>raw() );
        
        if ( l_receiver == null )
            return CFuzzyValue.from( false );
        
        l_receiver.trigger(
            CTrigger.from(
                ITrigger.EType.ADDGOAL,
                
                
                CLiteral.from(
                    "message/receive",
                    
                    CLiteral.from(
                        "message",
                        
                        
                        p_argument
                            .subList( 1, p_argument.size() )
                            .stream()
                            .map( i -> CRawTerm.from( i.raw() ) )
                    ),
                    
                    
                    
                    CLiteral.from(
                        "from",
                        CRawTerm.from(
                            p_context.agent().<MyCommunicationAgent>raw().name()
                        )
                    )
                )
            )
        );
        return CFuzzyValue.from( true );
    }
    
    @Nonnull
    final MyCommunicationAgent register( @Nonnull final MyCommunicationAgent p_agent )
    {
        m_agents.put( p_agent.name(), p_agent );
        return p_agent;
    }
    
    @Nonnull
    final MyCommunicationAgent unregister( @Nonnull final MyCommunicationAgent p_agent )
    {
        m_agents.remove( p_agent.name() );
        return p_agent;
    }
}

Variable-Builder

The variable builder allows to create individual variables and constants during runtime within a plan. In this case we create the constant MyName which stores the individual agent name. The raw-method allows to create an object reference with a safe-cast. The variable builder is added to the agent factory.


package myagentproject;
import org.lightjason.agentspeak.agent.IAgent;
import org.lightjason.agentspeak.language.execution.IVariableBuilder;
import org.lightjason.agentspeak.language.instantiable.IInstantiable;
import org.lightjason.agentspeak.language.variable.CConstant;
import org.lightjason.agentspeak.language.variable.IVariable;
import javax.annotation.Nonnull;
import java.util.stream.Stream;
final class CVariableBuilder implements IVariableBuilder
{
    @Nonnull
    @Override
    public final Stream<IVariable<?>> apply( @Nonnull final IAgent<?> p_agent, @Nonnull final IInstantiable p_runningcontext )
    {
        return Stream.of(
            new CConstant<>( "MyName", p_agent.<MyCommunicationAgent>raw().name() )
        );
    }
}

Reference Solution

This tutorial depends on the tutorial AgentSpeak-in-15min, so the whole build process is explained within the basic tutorial. If you struggled at some point or wish to obtain our exemplary solution with code documentation of this tutorial, you can download the archive containing the source code and an executable jar file:

If you run the example the shown output can be different. For the first run we start the program with 10 agents and 5 iterations:

agent 0    received message [   pcfhmqrkdcfo   ] from [   agent 2   ]
agent 0    received message [   eiwnfjhiqmcn   ] from [   agent 9   ]
agent 0    received message [   wevkklxbfbkp   ] from [   agent 4   ]
agent 0    received message [   mzlcztwrppss   ] from [   agent 8   ]
agent 0    received message [   tvnqxtlxfbuj   ] from [   agent 7   ]
agent 0    received message [   wgjgrponcrqp   ] from [   agent 1   ]
agent 0    received message [   kmplxsurilpd   ] from [   agent 0   ]
agent 0    received message [   sufdpibrcjvc   ] from [   agent 3   ]
agent 0    received message [   mxxiktkorrrd   ] from [   agent 5   ]
agent 0    received message [   zczagzgobfsf   ] from [   agent 6   ]

and run it again with equal arguments

agent 0    received message [   eafmluuggwde   ] from [   agent 6   ]
agent 0    received message [   nfckpeggfkwa   ] from [   agent 5   ]
agent 0    received message [   rtlbasuuoucp   ] from [   agent 9   ]
agent 0    received message [   jfnsinsfkpkr   ] from [   agent 1   ]
agent 0    received message [   lxedhrtkymxm   ] from [   agent 4   ]
agent 0    received message [   dyrpqwemcast   ] from [   agent 3   ]
agent 0    received message [   sxkbsmxrvttn   ] from [   agent 7   ]
agent 0    received message [   vvitgoirttnt   ] from [   agent 0   ]
agent 0    received message [   flwnyyekgmul   ] from [   agent 8   ]
agent 0    received message [   issvvzansmbl   ] from [   agent 2   ]

You can see that the agent 0 received messages in different ordering, so the executed plans are different. This behaviour is desired, because all agents run in parallel and so the agent can receive the message before its own cycle is called; otherwise the cycle is called and after that the agent receives the message. So keep in mind that all execution is heavily asynchronised and parallel.

Actions previous page
next page Environment