インタラクティブな会話からTodoリストのタスクを出力する方法 with Vertex AI
こんにちは。近藤です。普段はコミューンという会社でグローバルチームに所属し、グローバルマーケット向けの開発をしております。
本記事では、Todo管理を管理してほしい ~電話でTodo管理を自動化するサービス~で実装したユーザーとのインタラクティブな会話からタスクを抽出する方法を解説します。
なお使用言語はtypescriptです。
1. はじめに
今回サービスで実現したことを簡単に説明すると、以下の通りです。
【タスク作成が指定された時間に電話がかかってくる】
(AI Agent)「今日は何をしますか?」
(ユーザー)「今日はAとB、やります!」
(AI Agent)「AとBですね。承知しました。他に何かありますか?」
(ユーザー)「Cもやります」
(AI Agent)「ではAとBとCが今日やるタスクですね?他に何かありますか?」
(ユーザー)「ないです」
(AI Agent)「承知しました。では良い1日を」
→ A、B、C というタスクが作成されます。
タスク完了確認のパターン
【タスク完了確認が指定された時間に電話かかってくる】
(AI Agent)「今日はA,B,Cをやる予定でしたが、完了しましたか?」
(ユーザー)「今日はAとBだけ完了しました。」
(AI Agent)「AとBが完了したのですね。Cは完了しましたか?」
(ユーザー)「していません」
(AI Agent)「承知しました。お疲れ様でした」
→ A, B のタスクが完了に更新され、C はそのままとなります。
*「全部完了しました。」などの発言で全てのステータスが完了に変更されることもできます
2. ユーザーメッセージからのデータ出力
ここでは、会話モジュールの実装(conversation.service.ts)における変換処理の主要な流れを説明します。
会話モジュールの責務はユーザーのメッセージを解釈し、
- タスク(tasks)の作成および状態の更新をする
- ユーザーへのレスポンスをするためのメッセージ(message)を作成する
- 会話の状態(phase)を判定する
上記を行うにあたり、Vertex AIでは以下の様な実装をしています。
2.1 JSON 形式の出力を保証するプロンプト
会話モジュールでは、Vertex AI Gemini へ渡すプロンプトに、以下のようなルールを盛り込み、必ず JSON 形式の出力となるように指示しています。
const promptText = `
現在のフェーズは "${phase}" です。
既に認識しているタスク: ${JSON.stringify(tasks)}
ユーザー発話: "${userMessage}"
会話のルール:
- 出力はJSONで、必ず "phase", "message", "tasks" の3つを含む。
- "phase" は "ASK_PLAN", "CONFIRM_TASKS", "DONE" のいずれかを設定。
- "message" は次にユーザーへ話す文面。日本語でお願いします。
- "tasks" は現在管理しているタスク一覧を文字列配列で。
- ユーザーが "はい、お願いします" と言ったら、"DONE" に移行して終了メッセージを返す。
- ユーザーが新しいタスクを言ったら tasks に追加し、"CONFIRM_TASKS" にして確認メッセージを返す。
- フェーズが "ASK_PLAN" の場合は、「今日は何をしますか?」と聞く。
- 必ず有効な JSON オブジェクトを返してください。Markdown のコードフェンス(json や```)は付けないでください。
`;
このプロンプト設計により、Gemini はユーザーの発話を解析し、決められたスキーマに沿った JSON を返します。
更新の時のプロダクト
const promptText = `
現在のフェーズ: "${phase}"
当日のタスク一覧(未完了を含む): ${JSON.stringify(tasksForAi)} //DBから取得された未完了のタスクが入る
ユーザー発話: "${userMessage}"
会話のルール:
1. このフローは「タスクの完了確認」を行うためのものです。**新しいタスクは追加しないでください**。
2. "tasks" は { "description": string, "completed": boolean } の配列を必ず含みます。
3. 未完了のタスクを読み上げて、ユーザーにどれが終わったか尋ねてください。
4. ユーザーが「○○が終わった」「1番が終わった」などと言った場合は、該当するタスクの "completed" を true に設定します。
5. "phase" は "ASK_TODAY", "CONFIRM_TODAY", "DONE" のいずれかを設定してください。
- "ASK_TODAY": ユーザーにどれを完了したか尋ねる段階
- "CONFIRM_TODAY": 完了したタスクを確認し、「これで全部ですか?」などと尋ねる段階
- "DONE": すべてのタスク完了確認が終わった段階。終了メッセージを返す。
6. ユーザーが「はい、全部終わりました」「もう大丈夫です」などと言ったら、"phase" を "DONE" にして終了メッセージを返してください。
7. 出力形式:
{
"phase": "ASK_TODAY" | "CONFIRM_TODAY" | "DONE",
"message": string, // 次にユーザーへ話す日本語メッセージ
"tasks": [
{ "description": "...", "completed": true/false },
...
]
}
- Markdownのコードブロックや追加の文字は入れないでください。純粋なJSONのみを返してください。
8. 新しいタスクを作らないでください。あくまで "tasks" 内の既存タスクの completed フラグを更新するだけにしてください。
9. 可能な限り簡潔に日本語で問いかけや応答を作ってください。
10. 必ず有効な JSON オブジェクトを返してください。Markdown のコードフェンス(jsonや)は付けないでください。\`\`\`jsonは絶対に入れないでください。
`;
2.2 JSONモードの指定と出力フォーマットの指定
Vertex AI Gemini では、生成される出力を必ずJSON形式にするため、モデルの初期化時に以下のような設定を行います。
さらにresponseSchemaを指定することで、返却される JSON が決まった形式(ここでは phase、message、tasks を含むオブジェクト)となることが保証されます。
以下は、conversation.service.ts 内の実装例です。
import { VertexAI, type ResponseSchema, SchemaType } from "@google-cloud/vertexai";
// JSON スキーマの定義
const responseSchema: ResponseSchema = {
type: SchemaType.OBJECT,
properties: {
phase: {
type: SchemaType.STRING,
enum: ["ASK_PLAN", "CONFIRM_TASKS", "DONE"],
},
message: {
type: SchemaType.STRING,
},
tasks: {
type: SchemaType.ARRAY,
items: { type: SchemaType.STRING },
},
},
required: ["phase", "message"],
};
// プロンプトテキスト(上記で説明した promptText を利用)
const promptText = /* 上記の promptText の内容 */;
// リクエストオブジェクトの作成
const request = {
contents: [
{
role: "user",
parts: [
{
text: promptText,
},
],
},
],
generationConfig: {
maxOutputTokens: 512,
temperature: 0.5,
topP: 1.0,
topK: 32,
},
responseMimeType: "application/json",// JSONを指定
responseSchema,
};
// Vertex AI Gemini を利用してレスポンスを生成
const result = await conversationModel.generateContent(request);
この実装により、生成される出力は必ず JSON 形式となり、指定されたスキーマに沿っているため、後続処理でのパースエラーを防止できます。
2.3 レスポンスの整形とパース
実際の出力には、場合によっては Markdown のコードブロックなど余計な記述が含まれる可能性があるため、これを正規表現で除去してから JSON としてパースします。
const rawText = result.response?.candidates?.[0]?.content?.parts?.[0]?.text;
if (!rawText) {
return {
phase,
message: "会話中にエラーが発生しました。",
tasks,
};
}
const cleanedText = rawText.replace(/```[\s\S]*?```/g, "").trim();
const parsed = JSON.parse(cleanedText);
パース後の JSON は、会話の状態(phase)やタスク一覧(tasks)の更新に利用され、さらに後続の処理(タスクの最終確定や、ユーザーへのレスポンス)へとつながっていきます。
4. まとめ
これらの実装により、ユーザーとのインタラクティブな会話からタスク生成・更新が正確に行われるシステムを構築しています。
今回タスクのスキーマとして指定したのがdescriptionとcompletedのみでしたが、日時や期限なども入れることはできそうだなと思います。
Discussion