Open Floor Protocol   ›   Tutorials   ›   Building a Parrot Agent   ›   with BeaconForge PHP

Building a Parrot Agent with BeaconForge PHP

In this short guide, we will build a simple parrot agent together. The parrot agent will simply repeat everything you send it with a small 🦜 emoji in front of the return. We will create the Open Floor Protocol-compliant agent with the help of BeaconForge PHP (bfPHP).

TL;DR: The GitHub repository of the framework.


Initial Setup

BeaconForge PHP requires no package manager, no build step, and no dependencies. If your host can run PHP, it can run this agent.

You only need four files, two of which you will never touch:

File Edit? Purpose
run.php Rarely HTTP entry point
beaconforgeV3.php Never Core framework engine
agDef.json Yes Your agent's identity and capabilities
myAgFun.php Yes Your agent's conversational logic

Download all four files from the bfPHP directory and place them together in a folder on your server. A suggested structure:

public/parrot/
├── run.php
├── beaconforgeV3.php
├── agDef.json
└── myAgFun.php

Your agent's endpoint will be:

https://yourdomain.com/parrot/run.php

Now that the basic setup is done, let us start coding together!


Step 1: Configuring the Entry Point

Open run.php. You will find three lines you may want to configure:

$agentFunctionsFileName = 'myAgFun.php';  // Your agent logic file
$agentDefinitionJSON  = 'agDef.json'; // Your agent manifest file
$pathForPersistantStorage = ''; // Where to store conversation state

For development, you can leave all three as-is. For production, it is strongly recommended to store conversation state outside your public web directory so those files are not directly web-accessible:

// Example: if your public directory is at /home/username/public_html/
// store persistent data alongside it at /home/username/agentdata/
$pathForPersistantStorage = '../../../../agentdata/';

The framework will automatically create a subdirectory there named after your agent (e.g., agentdata/Parrot/). Make sure the path exists and is writable by PHP. That is all you need to change in run.php.


Step 2: Defining the Agent Manifest (agDef.json)

The agDef.json file is your agent's identity card. The framework reads it once at startup and sends it to floor managers whenever a getManifests event arrives.

Step 2.1: The top-level structure

Your agDef.json has three sections nested under a manifest key:

{
  "manifest": {
    "identification": { },
    "character":  { },
    "capabilities": { }
  }
}

Step 2.2: The identification section

This section is mandatory and defines the core identity of your agent. It is used by floor managers and discovery agents to find and address your agent.

"identification": {
  "serviceUrl": "https://yourdomain.com/parrot/run.php",
  "speakerUri": "tag:yourdomain.com,2026:parrot",
  "conversationalName": "Parrot",
  "role": "Echo Specialist",
  "department": "Demo",
  "organization": "OpenFloor Demo Corp",
  "synopsis": "A simple parrot agent that echoes back messages with a 🦜 emoji"
}

Why these fields?

  • speakerUri — The permanent identity of your agent. Use a tag: URI. This never changes, even if you move the agent to a new URL.
  • serviceUrlWhere the agent lives right now. This must exactly match the hosted URL of your run.php file.
  • conversationalName — The name your agent introduces itself by. It also doubles as the persistent storage subdirectory name, and the framework automatically sets $directedToMe = true whenever this name appears in an utterance.

Step 2.3: The character section

This is a BeaconForge-specific extension (not part of the core OFP spec) that hints to compatible clients how to visually render and voice your agent:

"character": {
  "headShot": "aParrot.png",
  "voice": {
    "vendor": "MS_EDGE",
    "name": "Zira",
    "uri":  "Microsoft Zira - English (United States)"
  }
}

Step 2.4: The capabilities section

This section is mandatory. Floor managers and discovery agents use it to decide whether to route a task to your agent.

"capabilities": {
  "keyphrases":  ["echo", "repeat", "parrot", "say"],
  "languages": ["en-us"],
  "descriptions":  [
    "Echoes back any text message with a 🦜 emoji",
    "Repeats user input verbatim",
    "Simple text mirroring functionality"
  ],
  "supportedLayers": ["text", "voice"]
}

What each field does:

  • keyphrases — Short searchable words used by simple text-based discovery.
  • descriptions — Full sentences used by AI-powered discovery agents.
  • languagesIETF BCP 47 language tags your agent supports.
  • supportedLayers — The dialog modalities your agent can handle (text, voice, ssml).

Step 2.5: The complete agDef.json

{
  "manifest": {
    "identification": {
      "serviceUrl": "https://yourdomain.com/parrot/run.php",
      "speakerUri": "tag:yourdomain.com,2026:parrot",
      "conversationalName": "Parrot",
      "role": "Echo Specialist",
      "department": "Demo",
      "organization": "OpenFloor Demo Corp",
      "synopsis": "A simple parrot agent that echoes back messages with a 🦜 emoji"
    },
    "character": {
      "headShot": "aParrot.png",
      "voice": {
        "vendor": "MS_EDGE",
        "name": "Zira",
        "uri":  "Microsoft Zira - English (United States)"
      }
    },
    "capabilities": {
      "keyphrases":  ["echo", "repeat", "parrot", "say"],
      "languages": ["en-us"],
      "descriptions":  [
        "Echoes back any text message with a 🦜 emoji",
        "Repeats user input verbatim",
        "Simple text mirroring functionality"
      ],
      "supportedLayers": ["text", "voice"]
    }
  }
}

Step 3: Building the Parrot Agent (myAgFun.php)

This is where the magic happens. Open myAgFun.php — you will implement the agentFunctions class, which extends the framework's baseAgentFunctions.

Step 3.1: The class skeleton and persistent state

Start with the constructor and the two state lifecycle hooks. The parent:: calls are required — they wire up the framework's save/restore machinery:

<?php
  class agentFunctions extends baseAgentFunctions {
    public function __construct( $fileName ) {
      parent::__construct( $fileName );
    }

Now add startUpAction(). This runs at the beginning of every request and restores your state from disk:

public function startUpAction() {
  parent::startUpAction(); // REQUIRED — restores $this->persistObject from disk

  if ( $this->persistObject == null ) {
    // First turn of a brand new conversation — initialize your state
    $this->persistObject = [
      'exchanges' => []
    ];
  }
}

And wrapUpAction(), which runs at the end of every request and saves your state back to disk:

public function wrapUpAction() {
  // Add any cleanup here before state is saved
  parent::wrapUpAction(); // REQUIRED — saves $this->persistObject to disk
}

**What **$this->persistObject** gives you:**

This is a plain PHP array that the framework automatically serializes to a JSON file after every turn and restores at the start of the next. You can store anything JSON-serializable in it, like the conversation history, user preferences, counters, and so on.

Step 3.2: Responding to an invitation

The inviteAction() method is called when a floor manager invites your agent to join a conversation. Return the string you want your agent to say, or '' to join silently:

public function inviteAction( $reason ) {
  return 'Hello! How can I help you today?';
}

Step 3.3: Implementing the parrot logic

utteranceAction() is the most important method. It is called every time anyone says anything in the conversation, that includes messages not directed at you.

Add the method signature and parameters:

public function utteranceAction( $heard, $fromUri, $directedToMe, $directedToSomeoneElse ) {
  // $heard — [string] what was said
  // $fromUri — [string] the speakerUri of who said it
  // $directedToMe  — [bool] true if explicitly addressed to this agent,
  //  OR if conversationalName appears in the utterance
  // $directedToSomeoneElse — [bool] true if explicitly addressed to a different agent

Now handle the three cases. First, if the utterance is directed at our agent this is where we parrot:

  if ( $directedToMe ) {
    // Combine the original text and add the 🦜 emoji prefix
    $parrotText = '🦜 ' . $heard;
    $say = $parrotText;

The parroting logic:

  • We simply prepend the 🦜 emoji to whatever was said.

Next, if it was directed to someone else we stay silent:

  } elseif ( $directedToSomeoneElse ) {
    $say = ''; // Stay silent — it was not meant for us

Finally, handle undirected (broadcast) utterances:

  } else {
    // Undirected utterance — we heard it but are not sure if it was for us
    $say = '';
  }

To finish the method, save the exchange to persistent state and return the response:

  // Log the exchange across conversation turns
  $this->persistObject['exchanges'][] = [
    'heard' => $heard,
    'said'  => $say
  ];

  return $say;
}
}
?>

Step 3.4: The complete myAgFun.php

<?php
  class agentFunctions extends baseAgentFunctions {

    public function __construct( $fileName ) {
      parent::__construct( $fileName );
    }

    public function startUpAction() {
      parent::startUpAction();
      if ( $this->persistObject == null ) {
        $this->persistObject = ['exchanges' => []];
      }
    }

    public function wrapUpAction() {
      parent::wrapUpAction();
    }

    public function inviteAction( $reason ) {
      return 'Hello! How can I help you today?';
    }

    public function utteranceAction( $heard, $fromUri, $directedToMe, $directedToSomeoneElse ) {
      if ( $directedToMe ) {
        $say = '🦜 ' . $heard;
      } elseif ( $directedToSomeoneElse ) {
        $say = '';
      } else {
        $say = '';
      }

      $this->persistObject['exchanges'][] = [
        'heard' => $heard,
        'said'  => $say
      ];

      return $say;
    }
  }
?>

Step 4: Test Your Implementation

Upload all four files to your server and send a test request. You can use our playground or curl to POST directly to your run.php endpoint.

Test an utterance directed at the parrot:

{
  "openFloor": {
    "conversation": { "id": "test_convo_001" },
    "sender": {
      "speakerUri": "ofpSocketClient",
      "serviceUrl": "https://testclient.example.com"
    },
    "events": [
      {
        "eventType": "utterance",
        "parameters": {
          "dialogEvent": {
            "features": {
              "text": {
                "tokens": [
                  { "value": "Hello Parrot, how are you?" }
                ]
              }
            }
          }
        }
      }
    ]
  }
}

The agent should respond with 🦜 Hello Parrot, how are you?

Test an invite:

{
  "openFloor": {
    "conversation": { "id": "test_convo_001" },
    "sender": {
      "speakerUri": "floorManager",
      "serviceUrl": "https://floormgr.example.com"
    },
    "events": [
      {
        "eventType": "invite",
        "to": { "speakerUri": "tag:yourdomain.com,2026:parrot" }
      }
    ]
  }
}

Request the manifest:

{
  "openFloor": {
    "conversation": { "id": "test_convo_001" },
    "sender": {
      "speakerUri": "ofpSocketClient",
      "serviceUrl": "https://testclient.example.com"
    },
    "events": [
      {
        "eventType": "getManifests",
        "to": { "speakerUri": "tag:yourdomain.com,2026:parrot" }
      }
    ]
  }
}

You can also use the simple single-file chat UI from azettl/openfloor-js-chat to test your agent locally.


How It Works (Under the Hood)

Here is the full request lifecycle so you can see how the framework and your code interact:

HTTP POST → run.php
│
▼
simpleProcessOFP() in beaconforgeV3.php
│
├─ Loads agDef.json, instantiates agentFunctions (myAgFun.php)
├─ Restores $this->persistObject from disk  ← startUpAction()
├─ Ignores messages sent by this agent itself
│
├─ For each OFP event in the request:
│  ├─ "invite" → inviteAction() → acceptInvite + utterance
│  ├─ "utterance"  → utteranceAction()  → utterance response
│  └─ "getManifests" → reads agDef.json → publishManifest
│
├─ Saves $this->persistObject to disk ← wrapUpAction()
└─ Returns complete OFP JSON response

The framework handles all OFP envelope formatting, sender/recipient routing, manifest serialization, and HTTP response construction. Your myAgFun.php code only ever deals with plain PHP strings, arrays, and simple booleans.