😇

Mastra で作った Deep Research の劣化版コピー Cheap Research をデプロイしました。

に公開

こんにちは、オートロの代表をしております。福田です。今回は、前にご紹介した「チープリサーチ:Mastra で Deep Research の模倣し、Open Cheap Research を作りました。」をデプロイしたことについてご紹介します。フロントエンドは、インターン生に作ってもらいました。

https://zenn.dev/open_developers/articles/ca49b32c4f8232

チープリサーチとは?

チープリサーチは、上記の記事でソースコードも公開されていますが、Mastra というエージェントアプリ開発フレームワークで開発した、Deep Research の劣化版クローンです。なので、利用価値はないのですが、勉強のためにデプロイまで行いました。

使い方

https://agentic.autoro.app/cheap-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