Agent Registry

Route inbound WhatsApp/SMS messages to a specific agent by phone number

Assign each of your agents a phone number, drop them in a registry, and the gateway dispatches every inbound WhatsApp/SMS message to the right agent. The registry is safe to consult and mutate from the concurrent inbound dispatch path.

python
from praisonaiagents import Agent
from praisonai_bot.bots import AgentRegistry

support = Agent(name="support", instructions="Help customers with product issues.")
sales   = Agent(name="sales",   instructions="Answer pricing and demo questions.")

registry = AgentRegistry(default_agent=support)
registry.assign("+1 (415) 555-0123", support)
registry.assign("+442071838750",    sales)

# The gateway calls this for every inbound message
agent = registry.resolve("+14155550123")  # → support
Diagram

Inbound

resolve

resolve

default_agent

📞 +1 415…

🗂️ AgentRegistry

📞 +44 20…

📞 unknown

🧠 Support Agent

🧠 Sales Agent

🧠 Triage Agent

Users experience it as three separate helplines backed by one gateway:

User textsNumberRouted toReply
Alice: “my order hasn’t arrived”+1 415 555 0123support agentShipment lookup
Bob: “what does the Pro plan cost?”+44 20 7183 8750sales agentPricing answer
Priya: brand-new numbernever seen beforetriage default agent“Who are you trying to reach?”

Quick Start

  1. Enable with a single agent

    One number, one agent. Normalisation means the formatting you assign with does not have to match the inbound format.

    python
    from praisonaiagents import Agent
    from praisonai_bot.bots import AgentRegistry
    
    agent = Agent(name="support", instructions="Be helpful.")
    
    registry = AgentRegistry()
    registry.assign("+14155550123", agent)
    
    registry.resolve("+1 (415) 555-0123")  # → agent  (normalisation matches)
  2. Multiple agents, multiple numbers

    Add a default_agent so unknown numbers still get answered.

    python
    from praisonaiagents import Agent
    from praisonai_bot.bots import AgentRegistry
    
    support = Agent(name="support", instructions="Product help.")
    sales   = Agent(name="sales",   instructions="Pricing and demos.")
    triage  = Agent(name="triage",  instructions="Greet new callers and route them.")
    
    registry = AgentRegistry(default_agent=triage)
    registry.assign("+14155550123", support)
    registry.assign("+442071838750", sales)
    
    registry.resolve("+14155550123")     # → support
    registry.resolve("+61 400 123 456")  # → triage (fallback)
  3. Wire it into a WhatsApp bot

    A real gateway calls registry.resolve(inbound_number) on every message, then hands the chat to the chosen agent. See WhatsApp Bot for the full bot setup.

    python
    from praisonai_bot.bots import AgentRegistry
    
    registry = AgentRegistry(default_agent=triage)
    registry.assign("+14155550123", support)
    registry.assign("+442071838750", sales)
    
    async def on_inbound(webhook):
        # `to_number` is the WhatsApp business number the user messaged
        agent = registry.resolve(webhook["to_number"])
        reply = await agent.chat(webhook["text"])
        return reply

How It Works

The gateway resolves the destination number to an agent, runs the chat, and replies on the same channel.

Diagram
🧠 Assigned Agent🗂️ AgentRegistry⚙️ Gateway📲 WhatsApp Cloud👤 User🧠 Assigned Agent🗂️ AgentRegistry⚙️ Gateway📲 WhatsApp Cloud👤 UserSend message to +1 415 555 0123Webhook (to = “+14155550123”)resolve(“+14155550123”)support agentchat(user_message)reply textReply📨 Reply
StepWhat happens
WebhookThe inbound message carries the business number the user texted (to_number)
Resolveregistry.resolve(to_number) returns the assigned agent, the default_agent, or None
ChatThe gateway runs the chosen agent with the user’s text
ReplyThe response is sent back on the same channel

Concurrent dispatch. The gateway runs many inbound messages at once. Share a single AgentRegistry across all of them — assign and resolve are protected by an internal lock, so simultaneous turns can never see a half-updated routing table.


Number Normalisation

"+1 (415) 555-0123" and "+14155550123" resolve to the same agent because normalize_number strips every non-digit and preserves a single leading +. Blank, non-string, or digit-free inputs normalise to None.

InputNormalised key
"+1 (415) 555-0123""+14155550123"
"+44 20 7183 8750""+442071838750"
"14155550123" (no leading +)"14155550123"
"", " ", None, non-stringNone

API Reference

AgentRegistry(default_agent=None)

In-memory phone-number → agent routing table. No I/O, no dependencies beyond the stdlib. Thread-safe: every method is guarded by a per-instance threading.Lock, and __iter__ returns a stable snapshot taken under the lock.

OptionTypeDefaultDescription
default_agent`Any \None`NoneFallback returned by resolve() when the inbound number is unknown or blank. When None, unknown numbers resolve to None.

Methods

MethodSignatureReturnsDescription
assignassign(number: str, agent: Any) -> strNormalised key (str)Assign number to agent. Re-assigning replaces the previous agent. Raises ValueError if number normalises to empty.
unassignunassign(number: str) -> boolboolRemove any agent for number. Returns True if a mapping was removed, else False.
resolveresolve(number: Optional[str]) -> Optional[Any]Agent or NoneReturn the agent assigned to number, else default_agent, else None.
numbersnumbers() -> List[str]List[str]All assigned (normalised) numbers.
__contains__number in registryboolMembership test using the normalised key.
__len__len(registry)intNumber of assigned numbers.
__iter__for key, agent in registryIterator[Tuple[str, Any]]Iterate (normalised_number, agent) pairs over a stable snapshot taken under the lock — mutations after iter(registry) cannot raise RuntimeError: dictionary changed size during iteration and do not retroactively alter the returned pairs.

normalize_number(number)

normalize_number(number: Optional[str]) -> Optional[str] returns the canonical key, or None when blank / non-string / no digits.

  • Strips whitespace and formatting characters (spaces, dashes, parens, dots).
  • Preserves a single leading +.

AgentRegistry vs ChannelDirectory

Pick by message direction: AgentRegistry routes inbound by phone number; ChannelDirectory routes outbound by target name.

Diagram

Inbound
(WhatsApp/SMS → agent)

Outbound
(agent → channel target)

Direction of the message?

AgentRegistry
routes by phone number

ChannelDirectory
routes by target name


Common Patterns

Fallback agent — set a default_agent so unknown numbers are never left unanswered.

python
from praisonaiagents import Agent
from praisonai_bot.bots import AgentRegistry

triage = Agent(name="triage", instructions="Greet new callers and route them.")
registry = AgentRegistry(default_agent=triage)

registry.resolve("+61 400 123 456")  # → triage (no assignment yet)

Dynamic re-assignment — remap or drop a number at runtime.

python
registry.assign("+14155550123", sales)   # move that number to sales
registry.unassign("+442071838750")       # take sales off that number
"+14155550123" in registry               # → True
len(registry)                            # → 1
list(registry)                           # → [("+14155550123", <Agent name='sales'>)]

Best Practices

Prefer E.164 numbers

Store numbers as +<country><digits> (E.164). Normalisation still handles human-typed variants like "+1 (415) 555-0123", but E.164 is unambiguous and matches what most webhooks send.

Build one registry per process

The registry holds references to already-constructed agents and does no I/O. Create it once at startup and consult it on every inbound message.

Always set a default_agent

Set AgentRegistry(default_agent=triage) if you never want to leave an inbound message unanswered. Without it, unknown numbers resolve to None and the caller must handle that case.

AgentRegistry is thread-safe

Every method is guarded by a per-instance threading.Lock, so you can call assign, unassign, and resolve from concurrent inbound turns without adding your own lock. Iterating with for key, agent in registry or list(registry) returns a stable snapshot taken under the lock — a concurrent mutation can never raise “dictionary changed size during iteration”.


Related