最近在开发一个基于Open Floor协议的对话代理时,我遇到了一个有趣的挑战:如何快速验证协议交互流程。于是我想到了构建一个最简单的代理形式——鹦鹉代理。这个代理的核心功能就是接收任何文本输入,并在前面加上一个🦜表情符号后原样返回。
Open Floor协议是一个用于构建对话式AI系统的开放标准,它定义了一套完整的消息交换格式和交互流程。通过实现这个简单的鹦鹉代理,我们可以深入理解协议的核心机制,包括:
这个项目虽然简单,但完整涵盖了Open Floor代理开发的所有关键环节,是学习协议和快速验证想法的理想起点。
首先我们需要建立一个标准的Node.js项目结构。我推荐使用以下目录布局:
code复制parrot-agent/
├── src/
│ ├── parrot-agent.ts # 代理核心逻辑
│ ├── server.ts # Express服务器
│ └── index.ts # 入口文件
├── tsconfig.json # TypeScript配置
└── package.json # 项目依赖
提示:使用TypeScript可以更好地处理Open Floor协议中复杂的类型定义,避免运行时类型错误。
执行以下命令初始化项目并安装必要依赖:
bash复制mkdir parrot-agent
cd parrot-agent
npm init -y
npm install express @openfloor/protocol
npm install -D typescript @types/node @types/express ts-node
关键依赖说明:
@openfloor/protocol:Open Floor协议的官方实现express:用于构建HTTP服务器typescript及相关类型定义:提供类型安全创建tsconfig.json文件,配置如下:
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"]
}
这个配置确保了:
在src/parrot-agent.ts中,我们首先定义代理类的基本结构:
typescript复制import {
BotAgent,
ManifestOptions,
UtteranceEvent,
Envelope,
createTextUtterance,
isUtteranceEvent
} from '@openfloor/protocol';
export class ParrotAgent extends BotAgent {
constructor(manifest: ManifestOptions) {
super(manifest);
}
async processEnvelope(incomingEnvelope: Envelope): Promise<Envelope> {
const responseEvents: any[] = [];
// 处理逻辑将在这里实现
return new Envelope({
schema: { version: incomingEnvelope.schema.version },
conversation: { id: incomingEnvelope.conversation.id },
sender: {
speakerUri: this.speakerUri,
serviceUrl: this.serviceUrl
},
events: responseEvents
});
}
}
processEnvelope方法是代理的核心,负责处理所有传入消息:
typescript复制async processEnvelope(incomingEnvelope: Envelope): Promise<Envelope> {
const responseEvents: any[] = [];
for (const event of incomingEnvelope.events) {
// 检查消息是否针对本代理
const addressedToMe = !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') {
// 处理能力查询请求
responseEvents.push({
eventType: 'publishManifest',
to: { speakerUri: incomingEnvelope.sender.speakerUri },
parameters: {
servicingManifests: [this.manifest.toObject()]
}
});
}
}
// 返回响应信封
return new Envelope({
// ...保持与上面相同的结构
});
}
添加私有方法处理具体的文本回复:
typescript复制private async _handleParrotUtterance(
event: UtteranceEvent,
incomingEnvelope: Envelope
): Promise<any> {
try {
const dialogEvent = event.parameters?.dialogEvent as { features?: any };
if (!dialogEvent?.features?.text?.tokens?.length) {
return createTextUtterance({
speakerUri: this.speakerUri,
text: "🦜 *chirp* I can only repeat text messages!",
to: { speakerUri: incomingEnvelope.sender.speakerUri }
});
}
const originalText = dialogEvent.features.text.tokens
.map((token: any) => token.value)
.join('');
return createTextUtterance({
speakerUri: this.speakerUri,
text: `🦜 ${originalText}`,
to: { speakerUri: incomingEnvelope.sender.speakerUri },
confidence: 1.0
});
} catch (error) {
console.error('Error in parrot utterance handling:', error);
return createTextUtterance({
speakerUri: this.speakerUri,
text: "🦜 *confused chirp* Something went wrong!",
to: { speakerUri: incomingEnvelope.sender.speakerUri }
});
}
}
为了方便创建代理实例,我们添加一个工厂函数:
typescript复制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'
} = 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'
]
}]
};
return new ParrotAgent(manifest);
}
在src/server.ts中设置Express应用:
typescript复制import express, { Request, Response } from 'express';
import { createParrotAgent } from './parrot-agent';
import { validateAndParsePayload } from '@openfloor/protocol';
const app = express();
app.use(express.json());
// CORS配置
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();
});
// 创建代理实例
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!'
});
// 主端点
app.post('/', async (req: Request, res: Response) => {
try {
console.log('Received request:', JSON.stringify(req.body, null, 2));
// 验证和解析负载
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;
const outgoingEnvelope = await parrotAgent.processEnvelope(incomingEnvelope);
// 发送响应
const responsePayload = outgoingEnvelope.toPayload();
res.json(responsePayload.toObject());
} catch (error) {
console.error('Error processing request:', error);
res.status(500).json({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
});
export default app;
src/index.ts作为应用入口:
typescript复制import app from './server';
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Parrot Agent server running on port ${PORT}`);
});
在package.json中添加以下脚本:
json复制{
"scripts": {
"start": "node dist/index.js",
"dev": "ts-node src/index.ts",
"build": "tsc"
}
}
使用以下命令启动开发服务器:
bash复制npm run dev
服务器将在http://localhost:8080启动。
可以使用cURL或Postman发送测试请求:
bash复制curl -X POST http://localhost:8080 \
-H "Content-Type: application/json" \
-d '{
"openFloor": {
"schema": { "version": "1.0" },
"conversation": { "id": "test-conversation" },
"sender": { "speakerUri": "user-123" },
"events": [{
"eventType": "utterance",
"parameters": {
"dialogEvent": {
"features": {
"text": {
"tokens": [{"value": "Hello"}]
}
}
}
}
}]
}
}'
预期响应应包含鹦鹉回复:
json复制{
"openFloor": {
"schema": { "version": "1.0" },
"conversation": { "id": "test-conversation" },
"sender": { "speakerUri": "tag:openfloor-demo.com,2025:parrot-agent" },
"events": [{
"eventType": "utterance",
"parameters": {
"text": "🦜 Hello"
}
}]
}
}
运行构建命令:
bash复制npm run build
编译后的代码将输出到dist目录。
创建.env文件配置生产环境:
code复制PORT=8080
SERVICE_URL=https://your-domain.com/
安装PM2并配置进程管理:
bash复制npm install -g pm2
pm2 start dist/index.js --name "parrot-agent"
pm2 save
pm2 startup
扩展代理类以支持简单对话记忆:
typescript复制export class ParrotAgent extends BotAgent {
private conversationHistory: Map<string, string[]> = new Map();
// ...其他代码不变
private async _handleParrotUtterance(
event: UtteranceEvent,
incomingEnvelope: Envelope
): Promise<any> {
const conversationId = incomingEnvelope.conversation.id;
// 获取或初始化对话历史
if (!this.conversationHistory.has(conversationId)) {
this.conversationHistory.set(conversationId, []);
}
const history = this.conversationHistory.get(conversationId)!;
// ...原有处理逻辑
// 添加到历史
history.push(originalText);
if (history.length > 10) history.shift(); // 限制历史长度
return createTextUtterance({
speakerUri: this.speakerUri,
text: `🦜 ${originalText} (I remember ${history.length} things!)`,
to: { speakerUri: incomingEnvelope.sender.speakerUri }
});
}
}
集成语言检测库:
bash复制npm install franc langdetect
扩展处理逻辑:
typescript复制import * as franc from 'franc';
import { detect } from 'langdetect';
private async _handleParrotUtterance(
event: UtteranceEvent,
incomingEnvelope: Envelope
): Promise<any> {
// ...原有文本提取逻辑
// 检测语言
const langCode = franc(originalText);
const langInfo = detect(originalText)[0];
return createTextUtterance({
speakerUri: this.speakerUri,
text: `🦜 ${originalText} (I think this is ${langInfo.lang}, ${langInfo.prob}% sure)`,
to: { speakerUri: incomingEnvelope.sender.speakerUri }
});
}
安装监控库:
bash复制npm install prom-client express-prom-bundle
扩展服务器:
typescript复制import promBundle from 'express-prom-bundle';
const metricsMiddleware = promBundle({
includeMethod: true,
includePath: true,
normalizePath: [
['^/.*', '/'] // 将所有路径归一化
]
});
app.use(metricsMiddleware);
// 添加专门的路由暴露指标
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});
安装限流中间件:
bash复制npm install express-rate-limit
配置限流:
typescript复制import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 每个IP限制100个请求
standardHeaders: true,
legacyHeaders: false
});
app.use(limiter);
扩展验证逻辑:
typescript复制app.post('/', async (req: Request, res: Response) => {
// 检查Content-Type
if (!req.is('application/json')) {
return res.status(415).json({ error: 'Unsupported Media Type' });
}
// 检查消息大小
if (Buffer.byteLength(JSON.stringify(req.body), 'utf8') > 1024 * 1024) {
return res.status(413).json({ error: 'Payload too large' });
}
// 原有验证逻辑...
});
实现简单的API密钥认证:
typescript复制app.use((req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (apiKey !== process.env.API_KEY) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
dockerfile复制FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
ENV PORT=8080
EXPOSE 8080
CMD ["node", "dist/index.js"]
bash复制docker build -t parrot-agent .
docker run -p 8080:8080 -e API_KEY=your-secret-key parrot-agent
在实现这个鹦鹉代理的过程中,我积累了一些有价值的经验:
协议版本兼容性:Open Floor协议仍在发展中,确保你的实现能够处理不同版本的信封结构。我在processEnvelope方法中特意保留了schema.version的传递。
错误恢复能力:对话系统可能会收到各种格式的消息,代理应该优雅地处理无效输入而不是崩溃。这就是为什么_handleParrotUtterance方法中有详细的错误检查。
性能考量:虽然这个代理很简单,但在生产环境中,我建议:
测试策略:为Open Floor代理编写测试时,要特别关注:
扩展思路:这个基础代理可以轻松扩展为更复杂的功能,比如:
这个鹦鹉代理虽然简单,但完整展示了Open Floor协议的核心概念和实现模式。通过这个项目,我深入理解了协议的设计哲学和实际应用中的各种考量。