javipalanca / spade

Smart Python Agent Development Environment
MIT License
253 stars 98 forks source link

Template sender jid matching not working with Prosody #74

Closed eager-seeker closed 4 years ago

eager-seeker commented 4 years ago

Description

When trying to use templates to match by sender jid, when using Prosody, random resource assignment seems to scupper the matching.

What I Did

I tried running 2 agents in different processes to force them to communicate via Prosody. There seemed no way to get template matching to work against sender jid, as the client was receiving messages with a random jid resource added:

import multiprocessing
multiprocessing.set_start_method('spawn', True)

import getpass
import time

from spade import quit_spade
from spade.agent import Agent
from spade.behaviour import OneShotBehaviour
from spade.message import Message
from spade.template import Template

class Pinger(Agent):
    class RequestBehav(OneShotBehaviour):
        async def run(self):
            self.agent.clear_dead_behaviours()
            msg = Message(to=self.agent.buddy_jid)  # Instantiate the message
            msg.set_metadata("performative", "request")
            msg.body = "Did you get it?"
            # Start listening:
            brec = self.agent.ReceiveInfoBehav()
            template = Template()
            template.sender = str(self.agent.buddy_jid)
            print(f"{template.sender} used as sender in template")
            template.set_metadata("performative", "inform")
            self.agent.add_behaviour(brec, template)
            # Send:
            await self.send(msg)
            print(f"{self.agent.jid.localpart} sent request")
            self.kill()

    class InformBehav(OneShotBehaviour):
        async def run(self):
            self.agent.clear_dead_behaviours()
            msg = Message(to=self.agent.buddy_jid)  # Instantiate the message
            msg.set_metadata("performative", "inform")
            msg.body = "Got it."
            await self.send(msg)
            print(f"{self.agent.jid.localpart} sent inform")
            self.kill()

    class ReceiveRequestBehav(OneShotBehaviour):
        async def run(self):
            print(f"{self.agent.jid.localpart} listening...")
            msg = await self.receive(timeout=10)  # wait for a message for 10 seconds
            if msg:
                print(f"{self.agent.jid.localpart} from {msg.sender} Recv: {time.time() - self.agent.req_sent} after request")
                binfo = self.agent.InformBehav()
                self.agent.add_behaviour(binfo)
            else:
                print("Did not receive any message after 10 seconds")

            self.kill()

    class ReceiveInfoBehav(OneShotBehaviour):
        async def run(self):
            msg = await self.receive(timeout=10)  # wait for a message for 10 seconds
            if msg:
                print(f"Info received by {self.agent.jid.localpart} from {msg.sender}")
            else:
                print("Did not receive any message after 10 seconds")

            self.kill()

    async def setup(self):
        if self.go_first:
            brec = self.ReceiveRequestBehav()
            template = Template()
            template.sender = str(self.buddy_jid)
            template.set_metadata("performative", "request")
            self.add_behaviour(brec, template)
        else:
            breq = self.RequestBehav()
            self.add_behaviour(breq)

        print(f"{self.jid.localpart} setup complete")

    def __init__(self, buddy_jid, go_first=False, *args, **kwargs):
        self.buddy_jid = buddy_jid
        self.go_first = go_first
        self.req_sent = time.time()
        super().__init__(*args, **kwargs)

def start_first_agent():
    agent0 = Pinger("agent1@localhost", True, "agent0@localhost", "Welcome1")
    future = agent0.start(auto_register=True)
    future.result()
    print("agent0 started")
    return

def start_second_agent():
    agent0 = Pinger("agent0@localhost", False, "agent1@localhost", "Welcome1")
    future = agent0.start(auto_register=True)
    future.result()
    print("agent1 started")
    return

def second_agent_worker(queue):
    start_second_agent()
    # queue.put("Second agent ready")

def first_agent_worker(queue):
    start_first_agent()
    queue.put("First agent ready and listening")

def start_in_two_procs():
    jobs = []
    queue = multiprocessing.Queue()

    p0 = multiprocessing.Process(target=first_agent_worker, args=(queue,))
    jobs.append(p0)
    p0.daemon = True
    p0.start()
    print("Starting queue get...")
    print(queue.get())
    print("... Finishing queue get.")

    p1 = multiprocessing.Process(target=second_agent_worker, args=(queue,))
    jobs.append(p1)
    p1.daemon = True
    p1.start()

    print("Processes started... Loop through some time.sleep calls...")
    for _ in range(0, 15):
        try:
            time.sleep(1)
        except KeyboardInterrupt:
            break

    queue.put("Knock it off.")

if __name__ == '__main__':
    start_in_two_procs()
    print("Bail!")

Expected a match for the message, instead, get:

No behaviour matched for message: <message to="agent0@localhost" from="agent1@localhost/50326214-d5fd-4160-8276-ac9660e8c9ee" thread="None" metadata={'performative': 'request'}>
Did you get it?
</message>

Noting the resource being added to the "from" jid, I tried debugging aioxmpp to see did it add it somewhere between send and receive, but it would appear not. In Prosody configuration, I see a setting that might add such a resource, but the default sounds fine. Explicitly setting to "kick_old" didn't seem to help: "conflict_resolve - How to resolve resource conflicts. May be "random" (assign a random resource), "increment" (append a unique integer to the resource), "kick_new" (deny the new connection), "kick_old" (disconnect the existing session). Default is "kick_old"."

mark-feeney-sage commented 4 years ago

Workaround in case it helps: The scenario above can be avoided simply by including an explicit resource in each jid before sending messages, that will be known when creating templates. e.g. agent0@localhost/dummyresource

javipalanca commented 4 years ago

Yes. Resources is an XMPP feature with which we have to deal. Maybe in next releases we could just work always with bare jids transparently.

eager-seeker commented 4 years ago

Closing as workaround of adding explicit resources works ok.