DEV Community

Andreas
Andreas

Posted on • Originally published at huggingface.co

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
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

Why these imports?

  • BotAgent - The base class we'll extend
  • ManifestOptions - To define our agent's capabilities
  • UtteranceEvent - The type of event we'll handle
  • Envelope - Container for Open Floor messages
  • createTextUtterance - Helper to create text responses
  • isUtteranceEvent - 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);
  }
Enter fullscreen mode Exit fullscreen mode

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> {
Enter fullscreen mode Exit fullscreen mode

Now let's build the method body step by step. First, create an array to store our responses:

    const responseEvents: any[] = [];
Enter fullscreen mode Exit fullscreen mode

Next, we loop through each event in the incoming envelope:

    for (const event of incomingEnvelope.events) {
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Why this check?

  • !event.to - If no recipient is specified, it's for everyone.
  • event.to.speakerUri === this.speakerUri - Direct message to us
  • event.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()]
          }
        });
      }
Enter fullscreen mode Exit fullscreen mode

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
    });
  }
Enter fullscreen mode Exit fullscreen mode

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 {
Enter fullscreen mode Exit fullscreen mode

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 }
        });
      }
Enter fullscreen mode Exit fullscreen mode

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 }
        });
      }
Enter fullscreen mode Exit fullscreen mode

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!
      });
Enter fullscreen mode Exit fullscreen mode

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 }
      });
    }
  }
Enter fullscreen mode Exit fullscreen mode

The only thing that is left to do is close the class with a closing brace:

}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

Step 2.2: Create the Express app

const app = express();
app.use(express.json());
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

Why this CORS setup?

  • Only allows requests from the specific domain
  • Handles preflight OPTIONS requests
  • Restricts to POST methods and the Content-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!'
});
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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
      });
    }
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Then let's process the envelope through our parrot agent:

    // Process the envelope through the parrot agent
    const outgoingEnvelope = await parrotAgent.processEnvelope(incomingEnvelope);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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'
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

Step 2.6: Export the app

export default app;
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Test Your Implementation

Run this to test:

npm run dev
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)