Editor Assistant API は、OpenAI AssistantAPI を使用して、マークダウンエディタの編集をサポートする機能です。主な要件は以下の通りです:
ストリーミング処理:
データ形式:
SseMessageSchema, SseDetectedDiffSchema, SseFinalizedSchema に準拠した JSON 形式{ message: "..." } と delta 形式の差分情報(insert, delete, retain)を含むエラーハンドリング:
効率性:
jsonrepair ライブラリ:
型定義:
message-error.ts: エラー型と定義schema.ts: エディタアシスタントのメッセージと差分の Zod スキーマ定義OpenAI API の仕様変更:
jsonrepair のアップデート:
パフォーマンス監視:
ストリーミング処理において、最大の課題は不完全なJSON文字列の処理です。OpenAI APIから部分的に届くJSONデータを即座に解析するために、以下の対策を実装しています:
具体例:
// ストリームから受け取った不完全なJSONの例
const partialJson = '{"contents": [{"message": "テキストを修正し';
// 通常のJSON.parseではエラー
// JSON.parse(partialJson); // SyntaxError: Unexpected end of JSON input
// jsonrepairを使用した修復
const repairedJson = jsonrepair(partialJson);
// 結果: '{"contents": [{"message": "テキストを修正しています"}]}'
// 修復されたJSONはパース可能
const parsedJson = JSON.parse(repairedJson);
// 結果: { contents: [{ message: 'テキストを修正しています' }] }
このように、正常なJSONとして完結していない途中のデータでも、jsonrepairは欠けている部分を補完して有効なJSONに変換します。OpenAI APIからの応答では、完全なJSONが揃うまで待つことなく、部分的に受信したデータを即座に処理できるようになります。
rawBufferの累積と継続的な解析:
rawBufferに累積し、その都度jsonrepairでパース可能な形に修復しています。エディタアシスタントの核心部分は、OpenAI APIからのレスポンスから差分情報を適切に抽出し、効率的にクライアントに送信する機能です。以下のような工夫を行っています:
メッセージと差分の処理の統合と最適化:
処理効率の向上メカニズム:
processedMessages Mapを使って、各メッセージ要素の前回の内容を記録し、差分のみを計算します。lastProcessedContentLength を用いて、すでに処理済みの要素をスキップします。これにより大量のデータでも効率的に処理できます。
// 処理開始位置の最適化 - 確定済み要素のスキップ
const startProcessingIndex = Math.max(0, Math.min(this.lastProcessedContentLength, contents.length) - 1);
// 単一ループでメッセージと差分を処理
for (let i = startProcessingIndex; i < contents.length; i++) {
// メッセージと差分の処理
}
OpenAIストリームの特性に対応した差分確定判定:
OpenAI APIからのJSONストリームは「前方から順に確定していく」特性があります。このAPIの特性を活用し、以下の判定ロジックを実装しています:
// 最終要素が変化した、またはこれが最終要素ではない場合 → 差分を確定とみなす
if (i < currentContentIndex || currentContentIndex > this.lastContentIndex) {
// 差分を確定して送信リストに追加
}
この条件判定は単なる技術的工夫ではなく、UXの向上を目的としています。確定していない差分を頻繁に送信すると、エディタが頻繁に更新されてユーザー体験が悪化するためです。
重複防止メカニズム:
getDiffKeyメソッドを実装しています。sentDiffKeys)を使うことで、O(1)の時間複雑度で効率的に重複チェックを行います。増分メッセージ計算の最適化:
getAppendedContentメソッドを実装しています。これにより、クライアントには新たに追加された部分のみを送信でき、通信量を大幅に削減できます。
private getAppendedContent(previousMessage: string, currentMessage: string): string {
// 前回のメッセージから増分部分のみを返す
return currentMessage.slice(previousMessage.length);
}
ストリーミング処理においてエラー耐性とリソース管理は特に重要です。以下の対策を講じています:
エラーハンドリングの階層化:
リソース解放の徹底:
destroyメソッドでメモリキャッシュをクリアし、イベントリスナーを解除することで、メモリリークを防止しています。非同期ストリーム処理の安全な終了:
このような設計と実装により、リアルタイム性と正確性を両立したエディタアシスタント機能を実現しています。ストリーミング処理の特性を活かしつつ、効率的なデータ処理と適応的な通知制御によって優れたユーザー体験を提供しています。