Mastra で作った Deep Research の劣化版コピー Cheap Research をデプロイしました。
こんにちは、オートロの代表をしております。福田です。今回は、前にご紹介した「チープリサーチ:Mastra で Deep Research の模倣し、Open Cheap Research を作りました。」をデプロイしたことについてご紹介します。フロントエンドは、インターン生に作ってもらいました。
チープリサーチとは?
チープリサーチは、上記の記事でソースコードも公開されていますが、Mastra というエージェントアプリ開発フレームワークで開発した、Deep Research の劣化版クローンです。なので、利用価値はないのですが、勉強のためにデプロイまで行いました。
使い方
こちらにアクセスして、チャットすれば利用できます。一応回数制限をつけています。チャットは公開情報として扱われるので、入力する情報には注意してください。 コールドスタートするので、閑散期には立ち上がりに時間がかかります。
構成
-
バックエンド
- サーバー:Hono
- クラウド:Cloud Run
-
フロントエンド
- フレームワーク:NextJS
- クラウド:Vercel
-
データベース
- DB: PostgreSQL
- クラウド:Supabase
-
エージェント
- フレームワーク:Mastra
- 言語モデル:Google Gemini 2.5
バックエンドを Cloud Run にしたのは、Playwright 使ったりと重めの処理がある独立したサーバーにしました。
フロントエンド
v0 とインターン生に開発してもらいました。AIのレスポンスをUIに表示したり、レポート結果をDocxでダウンロードするなどついていますが、特徴はなく v0 の出力を Copilot Agent で調整したくらいです。
Mastra では、メモリーにメッセージ履歴が含まれるためメッセージとして送信すると重複してしまいます(参考:公式ドキュメント)。なので、AI SDK の useChat のような便利なフックがそのままでは利用できません。面倒でしたが、自前で実装しました(といってもほとんどAIが書いてくれました)。
バックエンド
Mastra もカスタマイズしたりミドルウェアを追加したりとサーバーとして利用できるようですが、まだよくわからないので、Hono を使って Mastra を呼び出す仕組みにしています。
ストリーム部分は、hono/streaming の streamText を使いました。Thread にメタデータを付与したりする部分は、 onStepFinish
コールバックを使っています。調査が完了したことを記録しています。
return streamText(c, async (stream) => {
const result = await agent.stream([
{ role: 'user', content: query }
], {
resourceId: agent.id,
threadId,
onStepFinish: async (step) => {
if (isResearchStep(step)) {
agent.getMemory()?.saveThread({
thread: {
id: threadId,
title: thread!.title || 'Untitled Research',
resourceId: agent.id,
createdAt: thread!.createdAt,
updatedAt: new Date(),
metadata: {
userId: thread!.metadata?.userId,
researchCompleted: true,
} as { userId: string; researchCompleted?: boolean },
},
})
}
}
});
for await (const part of result.fullStream) {
const payload = (() => {
switch (part.type) {
case 'text-delta':
return { type: 'text', content: part.textDelta };
case 'tool-call':
return { type: 'tool-call', toolName: part.toolName, toolCallId: part.toolCallId, arg: part.args };
case 'tool-result':
return { type: 'tool-result', content: part };
case 'reasoning':
return { type: 'reasoning', content: part.textDelta };
case 'error':
return { type: 'error', message: part.error };
default:
return { type: 'unknown', content: part };
}
})();
await stream.write(`data: ${JSON.stringify(payload)}\n\n`);
}
stream.onAbort(() => {
console.log('Stream Aborted...');
});
stream.close();
});
}
streamText を使ってストリームを返却するようにしましたが、streamText を使わずに、return c.body(result.fullStream)
みたいにしてもOKでした。
データベース
匿名ユーザー管理とデータベースは、Supabase を使いました。hCaptcha でのいらずら防止も非常に簡単に導入できます。
今後
今後もエージェンティックなアプリケーションのサンプルを作っていきたいと思います。
Discussion