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.
The tutorial can be done in two steps:
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
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, "]")
.
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;
}
}
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 );
}
}
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;
}
}
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() )
);
}
}
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.