Building an Open Floor Parrot Agent
In this short guide, we will build a simple parrot agent together. The parrot agent will simply repeat everything you send him and a small ๐ฆ emoji in front of the return. We will create the Open Floor Protocol-compliant agent with the help of the @openfloor/protocol package.
Initial Setup
First, let's set up our project by creating the project folder and installing the required packages:
mkdir parrot-agent
cd parrot-agent
npm init -y
npm install express @openfloor/protocol
npm install -D typescript @types/node @types/express ts-node
We will also need a TypeScript configuration file, so create tsconfig.json
and add the following content:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Now that the basic set up is done, let us start coding together!
Step 1: Building the Parrot Agent Class
Before we create our parrot agent class, let's create a new folder src
where we will store all of our files.
Create a new file src/parrot-agent.ts
, this will contain the main logic of our agent.
Step 1.1: Add the imports
Lets start with the import of everything we need from the @openfloor/protocol
package, add them at the top of your parrot-agent.ts
file:
import {
BotAgent,
ManifestOptions,
UtteranceEvent,
Envelope,
createTextUtterance,
isUtteranceEvent
} from '@openfloor/protocol';
Why these imports?
BotAgent
- The base class we'll extendManifestOptions
- To define our agent's capabilitiesUtteranceEvent
- The type of event we'll handleEnvelope
- Container for Open Floor messagescreateTextUtterance
- Helper to create text responsesisUtteranceEvent
- To check if an event is an utterance
Step 1.2: Start the ParrotAgent class
Now let's start creating our ParrotAgent
class by extending the BotAgent
:
/**
* ParrotAgent - A simple agent that echoes back whatever it receives
* Extends BotAgent to provide parrot functionality
*/
export class ParrotAgent extends BotAgent {
constructor(manifest: ManifestOptions) {
super(manifest);
}
What we just did:
- Created a class that extends
BotAgent
- Added a constructor that takes a manifest and passes it to the parent class
- The manifest will define what our agent can do
Step 1.3: Override the processEnvelope method
The processEnvelope
method of the BotAgent
class is the main entry point for agent message processing. So, this is where the magic happens:
/**
* Override the processEnvelope method to handle parrot functionality
*/
async processEnvelope(incomingEnvelope: Envelope): Promise<Envelope> {
Now let's build the method body step by step. First, create an array to store our responses:
const responseEvents: any[] = [];
Next, we loop through each event in the incoming envelope:
for (const event of incomingEnvelope.events) {
We also should check if this event is meant for us. So, add this inside the loop:
// Check if this event is addressed to us
const addressedToMe = !event.to ||
event.to.speakerUri === this.speakerUri ||
event.to.serviceUrl === this.serviceUrl;
Why this check?
!event.to
- If no recipient is specified, it's for everyone.event.to.speakerUri === this.speakerUri
- Direct message to usevent.to.serviceUrl === this.serviceUrl
- Message to our service
With this check, we know the event is really meant for us, and we can now handle the two types of events we care about:
if (addressedToMe && isUtteranceEvent(event)) {
const responseEvent = await this._handleParrotUtterance(event, incomingEnvelope);
if (responseEvent) responseEvents.push(responseEvent);
} else if (addressedToMe && event.eventType === 'getManifests') {
// We respond to the getManifests event with the publishManifest event
responseEvents.push({
eventType: 'publishManifest',
// We use the senders speakerUri as the recipient
to: { speakerUri: incomingEnvelope.sender.speakerUri },
parameters: {
servicingManifests: [this.manifest.toObject()]
}
});
}
What's happening here:
- If it's a text message (utterance), we'll handle it with our parrot logic
- If someone asks for our capabilities via the
getManifests
event, we send back our manifest
To finish the method, we can now close the loop and return an envelope as a response with all the required response events:
}
// Create response envelope with all response events
return new Envelope({
schema: { version: incomingEnvelope.schema.version },
conversation: { id: incomingEnvelope.conversation.id },
sender: {
speakerUri: this.speakerUri,
serviceUrl: this.serviceUrl
},
events: responseEvents
});
}
Step 1.4: Implement the parrot logic
You saw in the processEnvelope
method that we call a yet undefined _handleParrotUtterance
, this is the private method we will now implement to echo back what we got sent via the utterance
event:
/**
* Handle utterance events by echoing them back
*/
private async _handleParrotUtterance(
event: UtteranceEvent,
incomingEnvelope: Envelope
): Promise<any> {
try {
First, let's try to extract the dialog event from the utterance:
const dialogEvent = event.parameters?.dialogEvent as { features?: any };
if (!dialogEvent || typeof dialogEvent !== 'object' || !dialogEvent.features || typeof dialogEvent.features !== 'object') {
return createTextUtterance({
speakerUri: this.speakerUri,
text: "๐ฆ *chirp* I didn't receive a valid dialog event!",
to: { speakerUri: incomingEnvelope.sender.speakerUri }
});
}
What we're doing:
- Extracting the dialog event from the utterance parameters
- Checking if it has the structure we expect
- If not, sending a friendly error message
Now, as we know we are dealing with a valid dialog event, we can try to get the text from it:
const textFeature = dialogEvent.features.text;
if (!textFeature || !textFeature.tokens || textFeature.tokens.length === 0) {
// No text to parrot, send a default response
return createTextUtterance({
speakerUri: this.speakerUri,
text: "๐ฆ *chirp* I can only repeat text messages!",
to: { speakerUri: incomingEnvelope.sender.speakerUri }
});
}
We only handle text, so as you see also here, we would return early with an createTextUtterance
and a generic message if the textFeature
is not how we expect it.
But now everything should be valid, and we can go for the actual parroting:
// Combine all token values to get the full text
const originalText = textFeature.tokens
.map((token: any) => token.value)
.join('');
// Create parrot response with emoji prefix
const parrotText = `๐ฆ ${originalText}`;
return createTextUtterance({
speakerUri: this.speakerUri,
text: parrotText,
to: { speakerUri: incomingEnvelope.sender.speakerUri },
confidence: 1.0 // Parrot is very confident in repeating!
});
The parroting logic:
- Extract text from tokens by mapping over them and joining
- Add the ๐ฆ emoji prefix
- Create a text utterance response
- Set confidence to 1.0 because ๐ฆ are confident!
Finally, we can add some error handling and close the method:
} catch (error) {
console.error('Error in parrot utterance handling:', error);
// Send error response
return createTextUtterance({
speakerUri: this.speakerUri,
text: "๐ฆ *confused chirp* Something went wrong while trying to repeat that!",
to: { speakerUri: incomingEnvelope.sender.speakerUri }
});
}
}
The only thing that is left to do is close the class with a closing brace:
}
Step 1.5: Add the factory function
After the class, add this factory function with the default configuration:
/**
* Factory function to create a ParrotAgent with default configuration
*/
export function createParrotAgent(options: {
speakerUri: string;
serviceUrl: string;
name?: string;
organization?: string;
description?: string;
}): ParrotAgent {
const {
speakerUri,
serviceUrl,
name = 'Parrot Agent',
organization = 'OpenFloor Demo',
description = 'A simple parrot agent that echoes back messages with a ๐ฆ emoji'
} = options;
const manifest: ManifestOptions = {
identification: {
speakerUri,
serviceUrl,
organization,
conversationalName: name,
synopsis: description
},
capabilities: [
{
keyphrases: ['echo', 'repeat', 'parrot', 'say'],
descriptions: [
'Echoes back any text message with a ๐ฆ emoji',
'Repeats user input verbatim',
'Simple text mirroring functionality'
]
}
]
};
return new ParrotAgent(manifest);
}
What this factory does:
- Takes configuration options
- Provides some defaults
- Creates a manifest that describes our agent's capabilities
- Returns a new ParrotAgent instance
Step 2: Building the Express Server
The agent itself is done, but how to talk to it? We need to build our express server for this, so start with creating a src/server.ts
file.
Step 2.1: Add imports
Add these imports at the top:
import express, { Request, Response } from 'express';
import { createParrotAgent } from './parrot-agent';
import {
validateAndParsePayload
} from '@openfloor/protocol';
Step 2.2: Create the Express app
const app = express();
app.use(express.json());
Step 2.3: Add CORS middleware
You might want to add a CORS configuration to allow access to your agent from different origins:
// CORS middleware for http://127.0.0.1:4000
const allowedOrigin = 'http://127.0.0.1:4000';
app.use((req, res, next) => {
if (req.headers.origin === allowedOrigin) {
res.header('Access-Control-Allow-Origin', allowedOrigin);
res.header('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type');
}
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
Why this CORS setup?
- Only allows requests from the specific domain
- Handles preflight
OPTIONS
requests - Restricts to
POST
methods and theContent-Type
header
Step 2.4: Create the agent instance
Now we need to create our parrot by using the factory function createParrotAgent
. Important is that the serviceUrl
matches your server endpoint; otherwise our agent will deny the request (remember the check we added in section 1.3).
// Create the parrot agent instance
const parrotAgent = createParrotAgent({
speakerUri: 'tag:openfloor-demo.com,2025:parrot-agent',
serviceUrl: process.env.SERVICE_URL || 'http://localhost:8080/',
name: 'Polly the Parrot',
organization: 'OpenFloor Demo Corp',
description: 'A friendly parrot that repeats everything you say!'
});
Step 2.5: Build the main endpoint step by step
Now we have the agent and the Express app, but the most important part is still missing, and that's our endpoint:
// Main Open Floor Protocol endpoint
app.post('/', async (req: Request, res: Response) => {
try {
console.log('Received request:', JSON.stringify(req.body, null, 2));
First, let's validate the incoming payload with the validateAndParsePayload
function from the @openfloor/protocol
package:
// Validate and parse the incoming payload
const validationResult = validateAndParsePayload(JSON.stringify(req.body));
if (!validationResult.valid) {
console.error('Validation errors:', validationResult.errors);
return res.status(400).json({
error: 'Invalid OpenFloor payload',
details: validationResult.errors
});
}
Now we know the payload is valid, and we can extract the envelope:
const payload = validationResult.payload!;
const incomingEnvelope = payload.openFloor;
console.log('Processing envelope from:', incomingEnvelope.sender.speakerUri);
Then let's process the envelope through our parrot agent:
// Process the envelope through the parrot agent
const outgoingEnvelope = await parrotAgent.processEnvelope(incomingEnvelope);
After the processing, we can create and send the response:
// Create response payload
const responsePayload = outgoingEnvelope.toPayload();
const response = responsePayload.toObject();
console.log('Sending response:', JSON.stringify(response, null, 2));
res.json(response);
Finally, we add the catch block and close the endpoint:
} catch (error) {
console.error('Error processing request:', error);
res.status(500).json({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
});
Step 2.6: Export the app
export default app;
Step 3: Creating the Entry Point
We end by creating a simple src/index.ts
as our entry point:
import app from './server';
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Parrot Agent server running on port ${PORT}`);
});
Step 4: Final Setup
Add or overwrite these scripts in the existing scripts
object in your package.json
:
{
"scripts": {
"start": "node dist/index.js",
"dev": "ts-node src/index.ts",
"build": "tsc"
}
}
Test Your Implementation
Run this to test:
npm run dev
Send your manifest or utterance requests to http://localhost:8080/
to see if it's working! You can also download the simple single HTML file manifest and utterance chat azettl/openfloor-js-chat to test your agent locally.
If you found this guide useful follow me for more and let me know what you build with it in the comments!