Modules design for multi-agent modeling

I’m using: SWI-Prolog version 7.6.4 and 8.0.2

Basically, I want the code to model knowledge bases for many agents. There is a common set of predicates that all the agents use, and each agent has its own percept info.

Approach taken:

Each agent stores their own data (e.g., perception) in their own module. For example, agt23:percept/1

All agents share static predicates for reasoning; those predicates are in a module called, say, kb, which exports many predicates. All agents will use that module kb, by using agtXX:use_module(kb). So, modules agtXX will have access to all predicates in module kb.

However, many predicates in kb make use of predicates defined in the specific calling agent module. For example, they use percept/1. For example, in module kb I have:

location(X) :- 
  percept(P),
  extract_location(X,P).

Now, because location(X) is in module kb, it tries to find percept/1 in kb and not in the module that called location/1.

My only solution was to use:

:- module_transparent(location/1).
location(X) :- 
  context_module(M),
  M:percept(P),
  extract_location(X,P).

This works but it is a lot of work as many many predicates in kb are meant to have goals against the calling module. The alternative is just to not use modules and have one huge database with all predicates, and duplicate them in every agent module.

Is there a best way to do this? Am I missing something on modules here?

Thanks a lot to whoever can give me a hint!

Sebastian

1 Like

Logtalk provides a straight-forward solution for multi-agents/multi-words design patterns. What you’re asking for is the ability to call predicates in self. Using your sample code:

:- object(kb).

    :- public([location/1, percept/1]).

    location(X) :- 
        ::percept(P),
        extract_location(X,P).

:- end_object.

:- object(agt23, extends(kb)).

    percept(foo).

:- end_object.
1 Like

Thanks @pmoura, makes total sense in an object-oriented setting.

Now, are you suggesting this cannot be done in SWI modules? I guess that is probably the case, but just want to confirm before finding another approach.

Note that the module transparent mechanism you describe is marked as deprecated in the documentation. But even if that’s not the case, that workaround, as other workarounds that are sometimes mentioned, require making explicit what’s implicit in Logtalk’s message to self control construct (::/1). E.g. extending most or all predicates with an extra argument. But that makes for cumbersome code (as you observed) and in itself doesn’t get you very far. Typical in multi-agents/multi-worlds applications, you also end up also requiring super calls, inheritance (e.g. to represent different kinds of agents or worlds), interfaces/protocols, … You have all that and more in Logtalk as it was designed to provide those features. But Prolog modules were simply not designed for these scenarios. No amount of hacking with Prolog modules will provide you with a clean, comprehensive, efficient, equivalent alternative. The required underpinnings are simply not there.

1 Like

As Paulo correctly remarks, module_transparent is indeed deprecated, though I do not expect it to be dropped anytime soon and probably never. Paulo is also right that the module system isn’t really designed for this and Logtalk is. That said, Logtalk is not more than a preprocessor for Prolog and if you need only one aspect of Logtalk you can quite easily replace this with some term/goal expansion and additional syntax that you like.

The normal way to do this would be for your KB module to have an additional argument for the agent and use that to make module qualified goals. From the agent module this would mean something like this (adjust syntax to your preference)

agent(Self) :-
    context_module(Self).

...

    ...,
    ^location(X),
    ....

where ^location(X) is translated using goal_expansion/2 into

    agent(Self),
    kb:location(X, Self)

And similarly you use term_expansion/2 in kb to add additional arguments and if you see e.g.,
::percept(P), you translate that into Agent:percept(P).

You can even omit the ^ and :: annotations by assuming any predicate defined in kb requires ^-transformation in the agent and in the kb, any not defined predicate requires the :: translation. In that scenario you do not have to change a letter to your bulk code, but you must write some not that simple expansion code.

It is not obvious what route is better. That depends a lot on your preferences, workflow, the rest of the application, etc. Personally I’d seriously consider the above if it is the only OO work you want/need. If you want more OO type abstractions in your code Logtalk is probably a better long term choice.

1 Like

That said, I’ve now seen so many cases where I would like to share variables with sub goals that I’m to some time seriously thinking about providing such a beast. Note that except for argument passing which is either tedious or requires term rewriting, there is no good way in Prolog. In conventional languages you can use a global variable, save the old value on the stack, assign it and restore after return (not thread safe), but in Prolog that doesn’t work due to additional backtracking control flow.

I don’t think it is technically hard to do. It basically means creating nested environments on the stack and search them to find a name/value pair. If people have ideas on how to expose this in a nice way to the Prolog programmer, I’m all ears. I guess one way is to provide some sort of lexical scope that exceeds clauses and the other is something like with_environment(Goal, [v1 = Val1, ...]). and predicates to get and set the environment variables.

1 Like

Hi Jan,

Thanks for your reply, I think I am almost there. I will see if I add LogTalk support in the next version, but that would have to be framed as a bit of a different project. For this particular, case I would like to see how far I can go with plain SWI.

Term/goal re-write is a good and clean approach. I understand how to expand the call to location(X) form the agent module, but didn’t get much of this:

The point is how to use term_expansion/2 in the kb so that the call to pecept(P) becomes Agent:percept(P).

The fact is that location(X) is a clause, not a goal. I have something like:

location(X) :-
   percept(P),
   member(loc(X), P).

And this would need to be converted via term_expansion/2 into:

location(X, Agent) :-
   Agent:percept(P),
   member(loc(X), P).

is that what you meant? But this requires expanding the head of the clause. Unfortunately, I cannot get much insight on the doc for term_expansion/2, but I have found some example in the doc for goal_expansion/2.

It is possible to just use term_expansion/2 to do the syntactic conversion of above? That would solve my problem I suspect.

Sebastian
PS: how good is this forum, the use of markdown and on-the-fly preview is just great!

Yes, that is the transformation you need to make. It basically means adding a rule for term_expansion/2 before the code in kb. That should look something like this:

:- module(kb, ...).

transform_kb_clause(ClauseIn, ClauseOut) :-
   ...

term_expansion(ClauseIn, ClauseOut) :-
    transform_kb_clause(ClauseIn, ClauseOut).

<the old code>

The term_expansion/2 rule gets the complete term read by the compiler and can translate it into anything it likes, including a list of terms to create multiple clauses or a combination of directives and clauses. The output is analysed and the goals are handed to goal_expansion/2.

Hope this helps. And yes, I think the move from Google groups to Discourse is a great improvement. Thank these guys for making this freely available to us, including free managed hosting.

1 Like

That’s like saying that Prolog is not more than a preprocessor for C. :slight_smile: It diverts the focus from the implemented semantics to the syntactic aspects. Its equivalent in the context of, say, constraints, would be to write that CLP(FD) is no more than a preprocessor and that you can easily replace it by hacking around is/2 with some term/goal expansion.

Of course, you can (re)implement Logtalk using the term-expansion mechanism. As I had the opportunity to publicly clarify several times, including in SWI-Prolog forums, that’s not the implementation solution I use as it would reduce the number of Prolog systems capable of running Logtalk from (currently 12) to 1.

Isn’t that definition for agent/1 is missing the :- module_transparent(agent/1). directive?

How do you see that working when the user types:

?- agt23:location(Location).

Sebastian is asking for a multi-agent application. With multiple agents loaded, you need to use explicitly module-qualified calls to call predicates such as location/1. In order to use your suggested ^location(X) solution, every single agent would need to have a local definition for all public predicates from the kb module that would make the super call in order to add the additional Self argument. Otherwise, as location/1 is now location/2, any query to an agent would also need to be term-expanded. But if you do that, you have an additional problem: which :/1 calls should be expanded and which :/1 calls should be left alone (note that it doesn’t matter if the qualification is implicit or explicit). To distinguish those calls you need machinery, book-keeping, …, maybe a different operator?

One consequence of your suggestion is that all kb predicates would need to be term-expanded to carry one additional argument as (1) they may need to use that argument or (2) they may need to pass it to a sub-goal for a predicate that needs it. But then you need to take care of knowing when to add that additional argument and when to omit it (e.g. calling a predicate defined elsewhere).

But there’s still more. Your suggestion is to compile a super call such as ^location(X) to an explicit :/2 goal with a fixed module argument. While the now explicit Self argument takes care of the fact that super calls preserve self, you’re now constrained (without additional hacks) to a flat hierarchy model. The reason why Logtalk’s ^^/1 control construct abstracts the object that contains the predicate definition that is eventually called is the same reason why the ::/2 control construct abstracts the object that received the original message: the required information may not be available at compile time. I.e. your suggested compilation of a super call is a possible compile-time optimization only when valid.

It should also be noted that, in the hack Sebastian describes, all public predicates in the kb module would need to be declared as module transparent. But that have more consequences than just enabling the use of context_module/1: it also changes the interpretation of any meta-call by the predicates.

As I illustrated above (and, to be clear, I only mentioned some of the hidden issues), even with annotations, the expansion code is “not that simple”. Your “quite easily replace” opening statement is challenged even in the limited scenarios discussed/hinted here. Add a few more sensible usage scenarios, typical of a multi-agent/multi-worlds applications, and you’re in trouble.

Indeed, clear application requirements, now and foreseen as the application evolves, and a clear understanding of pros and cons of each solution is key.

Cheers,
Paulo

1 Like

P.S. For some discussion of the pros and cons of Logtalk vs Prolog modules, see https://logtalk.org/rationale.html

2 Likes

Not really. Logtalk compiles to Prolog. Prolog isn’t compiled to C but to a VM that is implemented in C. Of course it is possible to compile Prolog to C, as wamcc does/did.

I’m not going to argue with you that Logtalk has better support for this than plain (SWI-)Prolog. It clearly has. It is not true that you cannot do such things in plain (SWI-)Prolog though. Its module system is pretty versatile.

The complete rewrite of the kb module to pass the additional argument is of course painful. Can we do better? Performance-wise probably not. If performance is less of an issue we have some alternatives. One is to use the user:exception/3 hook. You can use that to trap undefined calls in the agents. Considering location/1, we let the hook create a wrapper that provides access to self:

location(X) :-
     context_module(Self),
     with_self(Self, kb:location(X)).

And we define with_self/2 in e.g. kb as an exported predicate like below. The `no_lco_ is needed to avoid last call optimization while calling Goal. We pass this Module to avoid it being garbage collected.

with_self(Module, Goal) :-
     Goal,
     no_lco(Module).

no_lco(_).

Now we can define call_self/1 as

call_self(Goal) :-
    prolog_current_frame(F),
    prolog_frame_attribute(F, parent_goal, kb:with_self(Module, _)),
    call(Module:Goal).

And all we need to do is use goal expansion in kb to do the call_self. Wait, we can also use the above undefined predicate exception to define on the fly

percept(P) :-
    call_self(percept(P)).

Instead of doing this all on demand you can of course also add some meta description to the interface and generate all this in advance. We can also use meta-programing for the user call. There are endless options, providing trade offs between work, readability, maintainability and performance.

With Logtalk you get a well designed and documented total package. With plain Prolog you have to shape the world first, but you can keep it lightweight, make it do exactly what you want and make a trade off between all the options.

2 Likes

wow, this is indeed a very nice exchange.

I implemented a tou example using term_expansion/2 and goal_expansion/2 so that the KB can call to the corresponding “caller” module.

What I realized doing that is what both of you just pointed out:

The complete rewrite of the kb module to pass the additional argument is of course painful.

What makes it more complicated is that I don’t have the kb reasoning toolkit in just one module, but in many modules (actions, charge, items, etc…). So, a predicate X in the actions module may call one Y in charge module, which itself will need to post a goal Z against the original agent module (the one who called X in actions)! This means that the caller module should be passed across all predicates.

This makes me wonder that it is not so easy to design a mechanism, because in a sequence of goal calls, there will be many different contexts. which one we would want? the very first one? Why? not easy.

I am studying @jan last solution, it’s pretty smart, although using exceptions for this sounds a bit non-natural (of course, I am expecting everything can be done with any of this systems, as they are all turing-complete :slight_smile: )

Thanks nonetheless to both for the rich exchanges.

Sebastian
PS: @Jan, maybe would be good to add something like this as example for term_expansion/2?


:- module(test_kb, [far_away/2]).

% operator to signal when the predicate should be expanded with context module call
:- op(1, fx, ^).

% List all predicates that need to be added 1 more argument to track the calling module
transform_kb_clause(far_away(G):-Body, far_away(G, Agent):-Body2) :-
	add_agent_module(Agent, Body, Body2).

% domain-independent
term_expansion(ClauseIn, ClauseOut) :-
    transform_kb_clause(ClauseIn, ClauseOut).



% Add Agent as calling module in SOME predicates; here, only on percepts/1	
add_agent_module(Agent, ^G, Agent:G2) :- !,
	G =.. [F|Args],
	append(Args, [Agent], Args2),
	G2 =.. [F|Args2].
%add_agent_module(Agent, percepts(X), Agent:percepts(X)) :- !.
add_agent_module(Agent, (G1, G2), (G1_New, G2_New)) :- 
	add_agent_module(Agent, G1, G1_New),
	add_agent_module(Agent, G2, G2_New), !.
add_agent_module(Agent, (G1; G2), (G1_New; G2_New)) :- 
	add_agent_module(Agent, G1, G1_New),
	add_agent_module(Agent, G2, G2_New), !.
add_agent_module(Agent, (G1 -> G2), (G1_New -> G2_New)) :- 
	add_agent_module(Agent, G1, G1_New),
	add_agent_module(Agent, G2, G2_New), !.
add_agent_module(_, G, G).
	


% Is destination far from the current agent location?
% 	Agent location LocAgent is obtained via the percept of the agent, which is not in this module
% 	but in the agent "calling" module
far_away(Destination) :-
	^percepts(P),
	member(loc(LocAgent), P), !,
	distance(LocAgent, Destination, D),
	D > 10.
	

distance(L1, L2, D) :- D is abs(L1-L2).

will produce this clause when consulting:

far_away(D, A) :-
	call(A:percepts(B, A)),
	member(loc(C), B), !,
	distance(C, D, E),
	E>10.

What I do not like here are the add_agent_module/3 clauses to process the Body. I am sure it is not even complete!

You not need exceptions. Goal expansion in agents, wrapping all calls to the kb in e.g., call_kb/1 and goal expansion in the kb module that maps all undefined predicates to call_self/1 is enough. That might be more elegant. You probably want some declaration in the kb as to what you want to call in the agents and use that to decide on call_self/1 or not, e.g,

agent_hook(percept/1).
...

Actually, the latter job would need to be handled by goal_expansion. This requires some way to know the added argument. That can be done, but is quite far outside the documented (and thus supported) interface. You might get away calling expand_goal/2 on the body from your term expansion rules. In that case you can use a global variable (b_setval/2) to provide access to the added argument, so you get something like this

expand((Head0 :- Body0), (Head :- Body)) :-
     expand_head(Head0, Head, ExtraArg),
     b_setval(extra_arg, ExraArg),
     expand_goal(Body0, Body),
     b_setval(extra_arg, []).

I’m in favor of an example, but something typical and simple rather than this things along the edges of what one may want (and context arguments should be handled differently IMO).

1 Like

I stand corrected: That’s like saying that Prolog is not more than a preprocessor for VM instructions :slight_smile:

P.S. For readers not familiar with wamcc, it was the predecessor of GNU Prolog.

Logtalk is written in plain, portable, Prolog code, that runs in (among several systems) SWI-Prolog. Thus, its very existence is proof that you can do these things in plain (SWI)-Prolog. The difference is that in Logtalk the necessary work is already done. But doing it on top of the module system? All the solutions you are suggesting in this thread are arguably about working around, i.e. fighting the nature of the module system. There’s nothing fundamental in modules as a concept (when compared with objects) that justify that expect for the fact that there’s already an implementation in place.

I may be misunderstanding something in the semantics of prolog_frame_attribute/3 but… you may have, say, the user making a query agent:foo that you are transforming into a call to kb:foo, only that kb:foo calls kb:bar that in turn calls kb:baz with is the one making the call in self. Will parent_goal work in this case?

Also, as I mentioned earlier, you are effectively making location/1 a meta-predicate in order just ot be able to call context_module/1.

Logtalk indeed will add around ~460KB (if memory serves) to the application.

On the other hand, all Logtalk developer tools (debugger, documenting, testing, …) allow the user to work at the same abstraction level as the source code. In your suggested solution, the user will be e.g. tracing the expanded code, not the original source code.

Cheers,
Paulo

1 Like

I cannot help myself than remember here the Greenspun’s tenth rule: Greenspun's tenth rule - Wikipedia :smiley: :smiley: :smiley:

You can do this (and everything else that you have been discussing for your application) easily in Logtalk. Messages can be delegated, which preserves the original sender (the agent in your case where you want "to post a goal Z "). More to the point, you can do all this using only standard Logtalk constructs, without writing a single term-expansion/goal expansion rule, while saying at a nice abstraction level.

Cheers,
Paulo

We can emulate a notion of an execution environments with variables that are scoped to the children of a goal by

  • Creating a (often) meta call that makes these environment variables available through its arguments.
  • Make sure this goal stays on the stack while we execute the children (avoid LCO)
  • In the child, use SWI-Prolog’s runtime query routines on stack frames to find the variable value in the parent frame.

This does some of the work you could also do with delimited continuations, but cheaper and without any interference with Prolog’s semantics. That is all in all not very different than what you do in Logtalk: add a structure that is passed between all calls. In this case we do not need to pass anything. The price we pay is one intermediate call that cannot be subject to LCO and walking the stack if we need access to this variable (and portability, although any Prolog system should be able to do the same without much trouble). If the stack contains many intermediate frames and you need frequent access deep down you pay repetitive walks along the stacks.

Given thus though, we can make a super call, providing information on where we were called from and a self call that uses this information. You can use SWI-Prolog’s inheritance over modules to specialise modules. Surely there is a lot more good stuff in Logtalk, but if you merely want specialization and messages to sub/super you have all you need. Depending on how you implement it, the SWI-Prolog debugger should have no problems with this trick.

I’ve used that in the past to create a lightweight object system that interacted with XPCE classes.

Thanks for the detailed reply. I always expected something like you describe to be possible but never looked into the details (mainly because they are system specific). That provides an interesting venue to explore for a hypothetical alternative implementation of Logtalk (asa language) that would be more “native” in SWI-Prolog. Hope we can discuss this further soon. I should pay you a visit. Or you pay me a visit :slight_smile:

I’ve uploaded the old code to Github. Don’t think the application is still fully operational (although it still starts). The mini object system is in https://github.com/JanWielemaker/triple20/blob/c271a58a53afe222df62b3dc56237e57178a02b4/src/particle.pl

I think it does. Not unlikely we will be able to create some opportunity to explore that :slight_smile: Right now too busy with tabling …

1 Like

Another option is to reify by treating the ‘act’ as belief.

believes(bob, location(bob, paris)).
believes(alice, location(alice, new_york)).

then if alice has an opinion about where bob is,
believes(alice, location(bob, chicago)). % alice incorrectly believes bob is in chicago

an alternative to passing the agent name everywhere is b_setval(agent_context, bob)
then location does b_getval(agent_context, AgentName), percept(AgentName, P).

I’m wondering why you feel you should have a single percept for each agent, rather than
a bunch of components

video on term expansion

2 Likes