构建一个开放式鹦鹉代理

社区文章 发布于2025年7月18日

在这个简短的指南中,我们将一起构建一个简单的鹦鹉代理。这个鹦鹉代理会简单地重复你发送给它的所有内容,并在返回前加上一个小的🦜表情符号。我们将借助@openfloor/protocol包来创建符合Open Floor Protocol的代理。

初始设置

首先,让我们通过创建项目文件夹和安装所需软件包来设置我们的项目。

mkdir parrot-agent
cd parrot-agent
npm init -y
npm install express @openfloor/protocol
npm  install -D typescript @types/node @types/express ts-node

我们还需要一个TypeScript配置文件,所以创建tsconfig.json并添加以下内容

{
  "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"]
}

现在基本设置已经完成,让我们开始一起编写代码吧!

步骤1:构建鹦鹉代理类

在我们创建鹦鹉代理类之前,让我们创建一个新文件夹src,我们将所有文件都存储在这里。

创建一个新文件src/parrot-agent.ts,它将包含我们代理的主要逻辑。

步骤1.1:添加导入

让我们从导入@openfloor/protocol包中所需的一切开始,将它们添加到你的parrot-agent.ts文件的顶部。

import { 
  BotAgent, 
  ManifestOptions, 
  UtteranceEvent, 
  Envelope,
  createTextUtterance,
  isUtteranceEvent
} from '@openfloor/protocol';

为什么需要这些导入?

  • BotAgent - 我们将要扩展的基类
  • ManifestOptions - 用于定义我们代理的能力
  • UtteranceEvent - 我们将要处理的事件类型
  • Envelope - Open Floor消息的容器
  • createTextUtterance - 创建文本响应的助手
  • isUtteranceEvent - 检查事件是否为话语事件

步骤1.2:启动ParrotAgent类

现在让我们通过扩展BotAgent来创建我们的ParrotAgent

/**
 * 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);
  }

我们刚刚做了什么

  • 创建了一个扩展自BotAgent的类
  • 添加了一个构造函数,它接受一个清单并将其传递给父类
  • 清单将定义我们的代理能做什么

步骤1.3:重写processEnvelope方法

BotAgent类的processEnvelope方法是代理消息处理的主要入口点。因此,这就是奇迹发生的地方。

  /**
   * Override the processEnvelope method to handle parrot functionality
   */
  async processEnvelope(incomingEnvelope: Envelope): Promise<Envelope> {

现在让我们逐步构建方法主体。首先,创建一个数组来存储我们的响应。

    const responseEvents: any[] = [];

接下来,我们遍历传入信封中的每个事件

    for (const event of incomingEnvelope.events) {

我们还应该检查此事件是否是针对我们的。因此,将其添加到循环中

      // Check if this event is addressed to us
      const addressedToMe = !event.to || 
        event.to.speakerUri === this.speakerUri || 
        event.to.serviceUrl === this.serviceUrl;

为什么需要这个检查?

  • !event.to - 如果未指定收件人,则表示针对所有人。
  • event.to.speakerUri === this.speakerUri - 直接发送给我们的消息
  • event.to.serviceUrl === this.serviceUrl - 发送给我们的服务消息

通过这个检查,我们知道事件确实是针对我们的,现在我们可以处理我们关心的两种事件类型了

      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()]
          }
        });
      }

这里发生了什么

  • 如果它是文本消息(话语),我们将用我们的鹦鹉逻辑处理它。
  • 如果有人通过getManifests事件询问我们的能力,我们会返回我们的清单。

为了完成该方法,我们现在可以关闭循环并返回一个包含所有所需响应事件的信封作为响应。

    }

    // 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
    });
  }

步骤1.4:实现鹦鹉逻辑

您在processEnvelope方法中看到我们调用了一个尚未定义的_handleParrotUtterance,这是我们现在将实现的私有方法,用于回显通过utterance事件发送给我们的内容。

  /**
   * Handle utterance events by echoing them back
   */
  private async _handleParrotUtterance(
    event: UtteranceEvent, 
    incomingEnvelope: Envelope
  ): Promise<any> {
    try {

首先,让我们尝试从话语中提取对话事件。

      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 }
        });
      }

我们正在做什么

  • 从话语参数中提取对话事件
  • 检查它是否具有我们期望的结构
  • 如果不是,发送友好的错误消息

现在,由于我们知道我们正在处理一个有效的对话事件,我们可以尝试从中获取文本。

      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 }
        });
      }

我们只处理文本,所以您在这里也看到,如果textFeature不符合我们的预期,我们会通过createTextUtterance和通用消息提前返回。

但现在一切都应该是有效的,我们可以开始实际的鹦鹉学舌了。

      // 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!
      });

鹦鹉学舌逻辑

  • 通过遍历token并拼接,从token中提取文本
  • 添加🦜表情符号前缀
  • 创建文本话语响应
  • 将置信度设置为1.0,因为🦜是自信的!

最后,我们可以添加一些错误处理并关闭该方法。

    } 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 }
      });
    }
  }

剩下的唯一事情就是用一个闭合括号关闭类。

}

步骤1.5:添加工厂函数

在类之后,添加此工厂函数和默认配置

/**
 * 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);
}

这个工厂是做什么的

  • 接受配置选项
  • 提供一些默认值
  • 创建描述我们代理能力的清单
  • 返回一个新的ParrotAgent实例

步骤2:构建Express服务器

代理本身已经完成,但是如何与它对话呢?我们需要为此构建我们的Express服务器,所以从创建src/server.ts文件开始。

步骤2.1:添加导入

在顶部添加这些导入

import express, { Request, Response } from 'express';
import { createParrotAgent } from './parrot-agent';
import { 
  validateAndParsePayload
} from '@openfloor/protocol';

步骤2.2:创建Express应用程序

const app = express();
app.use(express.json());

步骤2.3:添加CORS中间件

您可能需要添加CORS配置,以允许您的代理从不同来源访问。

// 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();
});

为什么需要这个CORS设置?

  • 只允许来自特定域的请求
  • 处理预检OPTIONS请求
  • 限制为POST方法和Content-Type

步骤2.4:创建代理实例

现在我们需要使用工厂函数createParrotAgent创建我们的鹦鹉。重要的是serviceUrl要与您的服务器端点匹配;否则我们的代理将拒绝请求(记住我们在1.3节中添加的检查)。

// Create the parrot agent instance
const parrotAgent = createParrotAgent({
  speakerUri: 'tag:openfloor-demo.com,2025:parrot-agent',
  serviceUrl: process.env.SERVICE_URL || 'https://:8080/',
  name: 'Polly the Parrot',
  organization: 'OpenFloor Demo Corp',
  description: 'A friendly parrot that repeats everything you say!'
});

步骤2.5:逐步构建主端点

现在我们有了代理和Express应用程序,但最重要的部分仍然缺失,那就是我们的端点。

// Main Open Floor Protocol endpoint
app.post('/', async (req: Request, res: Response) => {
  try {
    console.log('Received request:', JSON.stringify(req.body, null, 2));

首先,让我们使用@openfloor/protocol包中的validateAndParsePayload函数验证传入的有效载荷。

    // 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
      });
    }

现在我们知道有效负载是有效的,我们可以提取信封。

    const payload = validationResult.payload!;
    const incomingEnvelope = payload.openFloor;

    console.log('Processing envelope from:', incomingEnvelope.sender.speakerUri);

然后让我们通过鹦鹉代理处理信封。

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

处理完后,我们可以创建并发送响应。

    // Create response payload
    const responsePayload = outgoingEnvelope.toPayload();
    const response = responsePayload.toObject();

    console.log('Sending response:', JSON.stringify(response, null, 2));

    res.json(response);

最后,我们添加 catch 块并关闭端点。

  } catch (error) {
    console.error('Error processing request:', error);
    res.status(500).json({
      error: 'Internal server error',
      message: error instanceof Error ? error.message : 'Unknown error'
    });
  }
});

步骤2.6:导出应用程序

export default app;

步骤3:创建入口点

最后,我们创建一个简单的src/index.ts作为我们的入口点。

import app from './server';

const PORT = process.env.PORT || 8080;

app.listen(PORT, () => {
  console.log(`Parrot Agent server running on port ${PORT}`);
});

步骤4:最终设置

在您现有的package.jsonscripts对象中添加或覆盖这些脚本。

{
  "scripts": {
    "start": "node dist/index.js",
    "dev": "ts-node src/index.ts",
    "build": "tsc"
  }
}

测试你的实现

运行此命令进行测试

npm run dev

https://:8080/发送您的清单或话语请求,看看它是否有效!您还可以下载简单的单HTML文件清单和话语聊天azettl/openfloor-js-chat来本地测试您的代理。

如果您觉得本指南有用,请关注我以获取更多内容,并在评论中告诉我您用它构建了什么!

社区

注册登录发表评论