
AI執事ボットを活用して、Googleカレンダーの予定をPythonで効率的に管理する方法を解説します。本記事では、同一時間帯に予定が重複しないよう自動判定する仕組みや、既存予定の更新に対応する構成の考え方を中心に紹介します。AIを取り入れた実用的なスケジュール管理を学びましょう。
予定削除・更新の概要
この記事では、AI執事ボットにおける予定の削除および更新機能について解説します。Googleカレンダーで登録済みの予定を扱う際、どのようにして「削除」または「更新」するかを、実装例を交えて紹介します。
予定削除と更新が必要な理由
予定削除や更新機能は、ユーザーの都合に応じてスケジュールを柔軟に変更できるようにするために不可欠です。たとえば、既存の予定を取り消したい、または内容や時間帯を変更したいというケースに対応するため、これらの機能が必要となります。
重複を防ぐ仕様と更新の考え方
本システムでは、「同一時間帯に既に予定が存在する場合、新しい予定は登録しない」ことで、重複による混乱を防止しています。更新を行いたい場合は、予定名と時刻が一致する既存の予定を削除したうえで、新しい予定を登録する構成としています。
更新処理の実際
更新処理では、ユーザーが送信したメッセージから予定のタイトルと日時を抽出し、該当時間帯に既存の予定が存在するかを確認します。もし一致する予定があれば、それを削除して新しい予定を登録する形で「更新」を実現します。
完成ソースコードと説明
次に、削除・更新機能を実装したコードを紹介します。以下は、カレンダー内のイベントを予定名と開始時刻で検索し、条件に一致するものを削除するための関数例です。
def deleteEvent(event_name, start_time):
jst = pytz.timezone("Asia/Tokyo")
credentials = getCredentials()
service = build("calendar", "v3", credentials=credentials)
now = datetime.now(jst)
past = now - timedelta(days=30)
future = now + timedelta(days=30)
events_result = service.events().list(
calendarId=os.getenv("GOOGLE_CALENDAR_ID"),
timeMin=past.isoformat(),
timeMax=future.isoformat(),
singleEvents=True,
orderBy="startTime"
).execute()
for event in events_result.get("items", []):
if event.get("summary") == event_name and event["start"].get("dateTime") == start_time:
service.events().delete(
calendarId=os.getenv("GOOGLE_CALENDAR_ID"),
eventId=event["id"]
).execute()
return f"予定『{event_name}』を削除しました。"
return f"予定『{event_name}』は見つかりませんでした。"
この関数では、ユーザーから送られた予定名と開始時刻の両方が一致するイベントをGoogleカレンダーから探し、該当するものを削除します。予定名だけでは重複イベントが判別できないため、時間情報を組み合わせた厳密な判定が必要です。
AI執事ボットに予定の削除・更新機能を追加

この記事では、AI執事ボットに予定の削除および更新機能を追加する方法を解説します。ユーザーが送信したメッセージに基づいて、既存の予定を特定し、削除または内容を更新する仕組みをPythonで実装します。
予定削除機能の概要
予定削除では、ユーザーが指定した「予定名」と「開始時刻」に一致するイベントをGoogleカレンダーから検索し、該当するものがあれば削除します。イベント名だけでは重複予定が区別できないため、時間情報を組み合わせた厳密な判定が必要です。
削除処理のイメージコード
def deleteEvent(event_name, start_time):
# summary と start.dateTime の両方が一致するイベントを削除
...
予定更新機能の概要
更新機能では、ユーザーから送信された新しい予定(タイトルと開始時刻)に基づいて、同一タイトル・同一時刻の既存予定を検索して削除しようとしますが、もしその時間帯にすでに予定が存在する場合は、登録せずにエラーメッセージを返します。
更新処理のイメージコード
def updateEvent(event_name, new_event_details):
deleteEvent(event_name, new_event_details["start_time"])
calendar_service.events().insert(
calendarId='primary',
body=new_event_details
).execute()
注意点
Googleカレンダーではイベント名が一意ではないため、削除・更新を安全に実行するには開始時刻も含めた照合が必要です。単に名前で一致させるだけでは、誤ったイベントを削除するリスクがあります。
前回記事との連携
この記事では、前回の記事「【Pythonの基礎知識】LINEに送るだけで予定が登録されるAI執事」と、今回追加された削除・更新機能との連携について解説します。
前回記事の内容の振り返り
前回の記事では、ユーザーがLINEで送信したメッセージ内容を元に、Googleカレンダーへ予定を登録するAI執事ボットの構築方法を紹介しました。自然文から予定のタイトルと時刻を抽出し、カレンダーに自動で追加するという基本的な動作が実現されていました。
「LINEに送るだけで予定が登録されるAI執事」の実現
この機能では、ユーザーが自然な言葉で予定を伝えるだけで、AIがそれを解釈し、Googleカレンダーに反映する仕組みが構築されました。たとえば「明日の14時に打ち合わせ」と送れば、そのままカレンダーに予定が追加されます。
新たに追加される削除・更新機能の位置づけ
今回追加された削除・更新機能により、予定の追加だけでなく、既存の予定を明示的に削除・再登録する形での変更にも対応できます。ただし、Googleカレンダーの仕様上、予定名のみでは一意にイベントを特定できないため、削除や更新には予定の開始時刻との組み合わせによる照合が必要です。
また、新しい予定を追加しようとした際に同じ時間帯に既存の予定がある場合は、登録を拒否する仕様となっており、意図しない重複を防ぐ仕組みも導入されています。
今後の記事の流れ
今後の記事では、こうした削除・更新の仕組みをどのように実装しているのかを、実際のコードとともに詳しく解説していきます。AI執事ボットが実用レベルでスケジュール管理をサポートできるようになるまでの全体像を把握できるように構成していきます。
完成ソースコードと実装例
この記事では、AI執事ボットに予定削除および更新機能を追加するための完成ソースコードと実装例を紹介します。具体的なコードとその解説を通じて、削除・更新機能をどのように実装するかを理解できます。
今回の機能追加では、Flask本体の
app.py には一切手を加える必要はありません。
自然文を処理して削除・更新の判断を行うのは
askChatgpt() 関数であり、ここから
chatgpt_logic.py と
calendar_utils.py に処理が分かれます。
したがって、編集対象はこの2ファイルのみであり、
app.pyは以前のままで問題なく動作します。
chatgpt_logic.pyの完成ソースコード
ユーザーからのメッセージを基に意図を分類し、予定の登録、削除、更新などの機能を実行する chatgpt_logic.py の完成ソースコードを紹介します。各処理はユーザーのリクエストに応じて適切に分岐し、Googleカレンダーとの連携を実現します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
import os import json from datetime import datetime from openai import OpenAI from dateutil.parser import parse from logic.calendar_utils import ( registerSchedule, getScheduleByOffset, deleteEvent, updateEvent ) # 🔍 ユーザーの発言から意図を判定(登録・更新・削除・予定確認など) def classifyIntent(user_input): user_input = user_input.lower() if "削除" in user_input: return "delete" elif "更新" in user_input or "変更" in user_input: return "update" elif "入れて" in user_input or "登録" in user_input or "追加" in user_input: return "register" elif "明後日" in user_input and "予定" in user_input: return "schedule+2" elif "明日" in user_input and "予定" in user_input: return "schedule+1" elif "今日" in user_input and "予定" in user_input: return "schedule+0" elif "予定" in user_input or "スケジュール" in user_input: return "schedule+0" elif "天気" in user_input: return "weather" elif "疲れた" in user_input or "やる気" in user_input: return "mental" else: return "general" # 📤 ChatGPTを使って予定のタイトルと(必要なら)開始時刻を抽出する def extractNewEventDetails(user_input, require_time=True): today = datetime.now().strftime("%Y-%m-%d") if require_time: system_content = ( f"あなたは自然文から予定の日時とタイトルを抽出するアシスタントです。\n" f"今日の日付は {today} です。『明日』『明後日』なども正しく認識してください。\n" f"絶対に自然文では返さず、以下の形式のJSONだけを返してください:\n" f"{{\"title\": \"予定名\", \"start_time\": \"2025-04-30 15:00:00\"}}\n" f"※形式が正しくないと処理ができません。" ) else: system_content = ( f"あなたは自然文から予定のタイトルだけを抽出するアシスタントです。\n" f"今日の日付は {today} です。『明日』『明後日』なども正しく認識してください。\n" f"絶対に自然文では返さず、以下の形式のJSONだけを返してください:\n" f"{{\"title\": \"予定名\"}}\n" f"※形式が正しくないと処理ができません。" ) messages = [ {"role": "system", "content": system_content}, {"role": "user", "content": user_input} ] client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) response = client.chat.completions.create( model="gpt-3.5-turbo", messages=messages ) content = response.choices[0].message.content print("📤 ChatGPTの返答(予定抽出):", content) try: parsed = json.loads(content) except json.JSONDecodeError: print("❌ JSON解析失敗:ChatGPT応答が不正な形式") raise ValueError("ChatGPTの応答が正しい形式ではありません。") # タイトルの正規化処理(ゆらぎ防止) title = parsed.get("title", "").strip() for junk in [ "の予定を変更", "の予定を削除", "の予定を追加", "の予定を登録", "を変更", "を削除", "を追加", "を登録", "の予定", "の予約", "予約" ]: title = title.replace(junk, "") title = title.strip() if require_time: start_time = parsed.get("start_time") return {"title": title, "start_time": start_time} else: return {"title": title} # 🗓️ 予定登録用:ChatGPTで抽出 → 登録処理 → 成功メッセージ返却 def registerScheduleFromText(user_message, client): try: new_event = extractNewEventDetails(user_message, require_time=True) title = new_event["title"] start_time = datetime.strptime(new_event["start_time"], "%Y-%m-%d %H:%M:%S") # ✅ 結果メッセージをそのまま返す result = registerSchedule(title, start_time) return result except Exception as error: print("❌ 予定登録エラー:", error) return "日付とタイトルの解析に失敗しました。" # 🎯 メイン処理:ユーザーの意図に応じて処理分岐し、結果を返す def askChatgpt(user_message): try: client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) intent = classifyIntent(user_message) print(f"🎯 intent 判定: {intent}") # 📆 特定日の予定確認(今日・明日・明後日など) if intent.startswith("schedule+"): day_offset = int(intent.split("+")[1]) return getScheduleByOffset(day_offset) # 📝 新規予定登録 elif intent == "register": return registerScheduleFromText(user_message, client) # 🗑️ 予定の削除(タイトル+開始時刻を厳密に抽出して削除) elif intent == "delete": try: new_event = extractNewEventDetails(user_message, require_time=True) title = new_event.get("title") start_time_raw = new_event.get("start_time") if not title or not start_time_raw: return "削除対象の予定が正しく抽出できませんでした。予定名と時間を明記してください。" start_time = datetime.strptime(start_time_raw, "%Y-%m-%d %H:%M:%S") return deleteEvent(title, start_time) except Exception as e: print("❌ 削除エラー:", e) return "削除中にエラーが発生しました。" # ♻️ 予定の更新(旧予定を削除 → 新予定を登録) elif intent == "update": try: new_event = extractNewEventDetails(user_message, require_time=True) title = new_event.get("title") start_time_raw = new_event.get("start_time") if not title or not start_time_raw: return "更新対象の予定が正しく抽出できませんでした。予定名と時間を明記してください。" start_time = datetime.strptime(start_time_raw, "%Y-%m-%d %H:%M:%S") return updateEvent(title, {"title": title, "start_time": start_time}) except Exception as e: print("❌ 更新エラー:", e) return "更新中にエラーが発生しました。" # 🤖 雑談など(ChatGPTへそのまま転送) messages = [ {"role": "system", "content": "あなたは親切で柔軟なAIアシスタントです。"}, {"role": "user", "content": user_message} ] response = client.chat.completions.create( model="gpt-3.5-turbo", messages=messages ) return response.choices[0].message.content except Exception as error: print("❌ ChatGPT応答全体エラー:", error) return "AI応答中にエラーが発生しました。" |
🔁 削除・更新処理に振り分けるための追加コード
このコードは、LINEから送られた自然文に対して「削除」または「更新」の意図を判別し、ChatGPTで予定名や日時を抽出して、それぞれの処理へ振り分けるロジックです。以下は askChatgpt() 関数の中で、意図が "delete" または "update" と判定されたときの処理部分です。
削除処理では、予定名( title)のみを抽出し、Googleカレンダー内の予定と照合します。イベント名は重複可能であるため、必要に応じて開始時刻との組み合わせで削除対象を特定する必要があります。
更新処理では、予定名と日時(start_time)を両方抽出し、「予定名+時刻が一致する既存の予定」があればそれを削除し、新しい日時で予定を再登録します。なお、同時間帯に他の予定が存在する場合は、登録は行われずエラーメッセージが返されます。
def askChatgpt(user_message):
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
intent = classifyIntent(user_message)
print(f"🎯 intent 判定: {intent}")
# ...(中略:登録・予定取得処理)...
elif intent == "delete":
try:
new_event = extractNewEventDetails(user_message, require_time=False)
title = new_event["title"]
return deleteEvent(title)
except Exception as e:
print("❌ 削除エラー:", e)
return "削除中にエラーが発生しました。"
elif intent == "update":
try:
new_event = extractNewEventDetails(user_message, require_time=True)
title = new_event["title"]
start_time = datetime.strptime(new_event["start_time"], "%Y-%m-%d %H:%M:%S")
return updateEvent(title, {"title": title, "start_time": start_time})
except Exception as e:
print("❌ 更新エラー:", e)
return "更新中にエラーが発生しました。"
このように、 askChatgpt() 内部でメッセージの意図を自動で判断し、ChatGPTによって抽出された情報を元に deleteEvent() や updateEvent() に処理を引き渡すことで、ユーザーは自然文だけで予定の削除や変更を行うことが可能になります。
chatgpt_utils.pyの完成ソースコード
ユーザーから送信されたメッセージを解析し、意図に基づいて適切な処理を実行する chatgpt_logic.py の完成ソースコードを紹介します。ユーザーのリクエストに応じて、予定の登録、削除、更新などの機能を提供するためのロジックが含まれています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
import os from datetime import datetime, timedelta import pytz from googleapiclient.discovery import build from google.oauth2 import service_account from dateutil.parser import parse # 🔐 Google API認証情報を取得 def getCredentials(): credentials_path = os.getenv("GOOGLE_SERVICE_ACCOUNT_JSON") if not credentials_path: raise ValueError("GOOGLE_SERVICE_ACCOUNT_JSON が未設定です") credentials = service_account.Credentials.from_service_account_file( credentials_path, scopes=["https://www.googleapis.com/auth/calendar"] ) return credentials # 📅 Googleカレンダーに予定を登録(30分間の固定枠) from pytz import timezone def registerSchedule(title, start_time): try: credentials = getCredentials() service = build("calendar", "v3", credentials=credentials) # JSTタイムゾーン付与 jst = timezone("Asia/Tokyo") if start_time.tzinfo is None: start_time = jst.localize(start_time) end_time = start_time + timedelta(minutes=30) calendar_id = os.getenv("GOOGLE_CALENDAR_ID") if not calendar_id: raise ValueError("GOOGLE_CALENDAR_ID が未設定です") # 🔍 重複チェック:同時間帯に予定があるか確認 events_result = service.events().list( calendarId=calendar_id, timeMin=start_time.isoformat(), timeMax=end_time.isoformat(), singleEvents=True, orderBy="startTime" ).execute() events = events_result.get("items", []) if events: print("⚠️ 同時間帯に既に予定があります") return "その時間にはすでに予定が登録されています。別の時間を指定してください。" # ✅ 登録処理 event = { "summary": title, "start": {"dateTime": start_time.isoformat(), "timeZone": "Asia/Tokyo"}, "end": {"dateTime": end_time.isoformat(), "timeZone": "Asia/Tokyo"} } created = service.events().insert(calendarId=calendar_id, body=event).execute() print("✅ 登録イベント情報:", created) return f"予定『{title}』を登録しました。" except Exception as error: print("❌ 登録エラー:", error) return "予定の登録中にエラーが発生しました。" # 📆 任意日数後の予定を取得 def getScheduleByOffset(day_offset: int): credentials = getCredentials() service = build("calendar", "v3", credentials=credentials) jst = pytz.timezone("Asia/Tokyo") target_date = datetime.now(jst) + timedelta(days=day_offset) start = datetime(target_date.year, target_date.month, target_date.day, 0, 0, 0, tzinfo=jst).isoformat() end = datetime(target_date.year, target_date.month, target_date.day, 23, 59, 59, tzinfo=jst).isoformat() calendar_id = os.getenv("GOOGLE_CALENDAR_ID") if not calendar_id: raise ValueError("GOOGLE_CALENDAR_ID が未設定です") events_result = service.events().list( calendarId=calendar_id, timeMin=start, timeMax=end, singleEvents=True, orderBy="startTime" ).execute() events = events_result.get("items", []) label = {0: "今日", 1: "明日", 2: "明後日"}.get(day_offset, f"{day_offset}日後") if not events: return f"{label}の予定はありません。" result = f"{label}の予定はこちらです:\n" for event in events: start_time = event["start"].get("dateTime", event["start"].get("date")) result += f"・{start_time}:{event['summary']}\n" return result # 🗑️ 予定を名前と時刻で削除(JSTベースの30日前〜30日後範囲) from dateutil.parser import parse # 必須 def deleteEvent(event_name, start_time): try: # ✅ タイトルを正規化 for junk in ["の予定", "の予約", "予約"]: event_name = event_name.replace(junk, "") event_name = event_name.strip() credentials = getCredentials() service = build("calendar", "v3", credentials=credentials) jst = pytz.timezone("Asia/Tokyo") now = datetime.now(jst) past = now - timedelta(days=30) future = now + timedelta(days=30) # 🔍 文字列なら datetime に変換 if isinstance(start_time, str): target_start = parse(start_time) else: target_start = start_time events_result = service.events().list( calendarId=os.getenv("GOOGLE_CALENDAR_ID"), timeMin=past.isoformat(), timeMax=future.isoformat(), singleEvents=True, orderBy="startTime" ).execute() for event in events_result.get("items", []): event_start_str = event["start"].get("dateTime") if not event_start_str: continue event_start = parse(event_start_str) # 🔁 厳密比較ではなく tz/microsec を除外して比較 if (event.get("summary") == event_name and event_start.replace(tzinfo=None, microsecond=0) == target_start.replace(tzinfo=None, microsecond=0)): service.events().delete( calendarId=os.getenv("GOOGLE_CALENDAR_ID"), eventId=event["id"] ).execute() print("✅ 削除成功:", event_name) return f"予定『{event_name}』を削除しました。" return f"予定『{event_name}』は見つかりませんでした。" except Exception as error: print("❌ 削除エラー:", error) return "予定削除中にエラーが発生しました。" # 🔁 旧予定を削除した上で、新しい内容で再登録する更新処理(重複チェックなし) def updateEvent(event_name, new_event): try: # 認証とサービス初期化 credentials = getCredentials() service = build("calendar", "v3", credentials=credentials) jst = pytz.timezone("Asia/Tokyo") now = datetime.now(jst) past = now - timedelta(days=30) future = now + timedelta(days=30) calendar_id = os.getenv("GOOGLE_CALENDAR_ID") if not calendar_id: raise ValueError("GOOGLE_CALENDAR_ID が未設定です") # 🔍 過去30日〜未来30日から「タイトル一致の予定」を検索 events_result = service.events().list( calendarId=calendar_id, timeMin=past.isoformat(), timeMax=future.isoformat(), singleEvents=True, orderBy="startTime" ).execute() events = events_result.get("items", []) deleted = False # 🔥 最初に見つけた「予定名一致」の予定を削除(旧予定) for event in events: if event.get("summary") == event_name: service.events().delete( calendarId=calendar_id, eventId=event["id"] ).execute() print("✅ 旧予定を削除:", event_name) deleted = True break # 複数一致しても一件だけ更新する方針 if not deleted: return f"予定『{event_name}』は見つかりませんでした。" # ✅ 新しい予定を直接登録(重複チェックせず) new_title = new_event["title"] new_start_time = new_event["start_time"] if new_start_time.tzinfo is None: new_start_time = jst.localize(new_start_time) new_end_time = new_start_time + timedelta(minutes=30) event_body = { "summary": new_title, "start": { "dateTime": new_start_time.isoformat(), "timeZone": "Asia/Tokyo" }, "end": { "dateTime": new_end_time.isoformat(), "timeZone": "Asia/Tokyo" } } created = service.events().insert( calendarId=calendar_id, body=event_body ).execute() print("✅ 新予定を登録:", created.get("summary")) return f"予定『{event_name}』を新しい内容で更新しました。" except Exception as error: print("❌ 更新エラー:", error) return f"更新中にエラーが発生しました:{error}" |
🗑️ 削除処理の追加コード
以下は、Googleカレンダーから予定を削除する処理です。ユーザーから送信された自然文から抽出した予定名( title)と、開始時刻( start_time)の両方が一致するイベントを、過去30日〜未来30日の範囲から検索し、見つかれば削除します。
def deleteEvent(event_name, start_time):
jst = pytz.timezone("Asia/Tokyo")
credentials = getCredentials()
service = build("calendar", "v3", credentials=credentials)
now = datetime.now(jst)
past = now - timedelta(days=30)
future = now + timedelta(days=30)
events_result = service.events().list(
calendarId=os.getenv("GOOGLE_CALENDAR_ID"),
timeMin=past.isoformat(),
timeMax=future.isoformat(),
singleEvents=True,
orderBy="startTime"
).execute()
for event in events_result.get("items", []):
if event.get("summary") == event_name and event["start"].get("dateTime") == start_time:
service.events().delete(
calendarId=os.getenv("GOOGLE_CALENDAR_ID"),
eventId=event["id"]
).execute()
return f"予定『{event_name}』を削除しました。"
return f"予定『{event_name}』は見つかりませんでした。"
処理の流れ:
- 認証:GoogleサービスアカウントでAPIに接続
- 検索:過去30日〜未来30日から予定名と時刻が一致するイベントを検索
- 削除:一致する予定があれば即削除
- 結果:削除の可否をLINEに返信
🔁 更新処理の追加コード
以下は、既存の予定を一度削除し、新しい内容で再登録することで「更新」を実現する処理です。AI執事ボットでは、Googleカレンダーの仕様に従い、既存の予定と同じ時間帯に別の予定を登録する場合でも、先に旧予定を削除してから再登録を行うことで、予定の内容や時間を柔軟に変更できるように設計されています。
ポイント:
- 登録前に重複チェックは行いません(既存予定を削除しているため)
- 登録処理は registerSchedule() を使わず、直接 insert() を呼び出します
- これにより「旧予定と同じ時間への更新」も安全に処理可能です
# 🔁 旧予定を削除した上で、新しい内容で再登録する更新処理(重複チェックなし)
def updateEvent(event_name, new_event):
jst = pytz.timezone("Asia/Tokyo")
credentials = getCredentials()
service = build("calendar", "v3", credentials=credentials)
now = datetime.now(jst)
past = now - timedelta(days=30)
future = now + timedelta(days=30)
calendar_id = os.getenv("GOOGLE_CALENDAR_ID")
if not calendar_id:
raise ValueError("GOOGLE_CALENDAR_ID が未設定です")
# 🔍 過去30日〜未来30日から「タイトル一致の予定」を検索し、削除
events_result = service.events().list(
calendarId=calendar_id,
timeMin=past.isoformat(),
timeMax=future.isoformat(),
singleEvents=True,
orderBy="startTime"
).execute()
deleted = False
for event in events_result.get("items", []):
if event.get("summary") == event_name:
service.events().delete(
calendarId=calendar_id,
eventId=event["id"]
).execute()
deleted = True
break
if not deleted:
return f"予定『{event_name}』は見つかりませんでした。"
# ✅ 新しい予定を直接登録(重複チェックなし)
new_title = new_event["title"]
new_start_time = new_event["start_time"]
if new_start_time.tzinfo is None:
new_start_time = jst.localize(new_start_time)
new_end_time = new_start_time + timedelta(minutes=30)
event_body = {
"summary": new_title,
"start": {
"dateTime": new_start_time.isoformat(),
"timeZone": "Asia/Tokyo"
},
"end": {
"dateTime": new_end_time.isoformat(),
"timeZone": "Asia/Tokyo"
}
}
created = service.events().insert(
calendarId=calendar_id,
body=event_body
).execute()
print("✅ 新予定を登録:", created.get("summary"))
return f"予定『{event_name}』を新しい内容で更新しました。"
処理の流れ:
- 1. 認証情報を取得し、GoogleカレンダーAPIに接続
- 2. 過去30日〜未来30日の範囲で、タイトルが一致するイベントを検索
- 3. 該当イベントが見つかれば削除
- 4. 削除が成功したら、新しいタイトル・日時で予定を再登録(重複チェックなし)
- 5. 処理結果をLINEなどのチャットに応答として返す
全体の流れ
以下は、LINEからのリクエストを受け取って、Googleカレンダーに予定の削除・更新を実行するフローです。
🧭 アプリ全体の処理フローと構成
このAI執事ボットでは、ユーザーがLINEに送ったメッセージが以下の流れで処理され、Googleカレンダーへの予定削除・更新に反映されます。
- Webhook受信とエンドポイント処理(app.py)
LINEから送られたメッセージは、Flaskアプリの /callback エンドポイントで受信されます。受け取ったテキストはそのまま askChatgpt() 関数に渡され、以降の処理を一任します。 - メッセージの意図分類(chatgpt_logic.py)
askChatgpt() 関数内では、まず classifyIntent() によってメッセージの内容を解析し、以下のような意図に分類されます:- register(登録)
- delete(削除)
- update(更新)
- schedule(予定確認)
- その他:weather / mental / general など
- ChatGPTで予定情報を抽出(chatgpt_logic.py)
削除・更新の場合、意図分類後に extractNewEventDetails() 関数を使って ChatGPT に自然文を解析させます。
この関数は、予定のタイトル(削除時)やタイトル+日時(更新時)をJSON形式で返します。
※予定名だけでなく、開始時刻も含まれていないと正しく処理できない場合があります。 - Googleカレンダー操作(calendar_utils.py)
抽出された情報をもとに、以下のようにカレンダーAPIが呼び出されます:- deleteEvent():予定名と開始時刻の両方が一致するイベントを検索し、該当すれば削除
- updateEvent():旧予定を検索して削除後、指定の日時で新たな予定を再登録
- 処理結果の返信(LINE)
askChatgpt() から返されたメッセージ(例:「予定『歯医者』を削除しました」)が、そのままLINEユーザーに返信されます。
これにより、ユーザーはLINEに話しかけるだけで、予定の削除・変更を完了できます。
🔍 classifyIntent() の役割
classifyIntent() 関数は、ユーザーの自然文からキーワードを抽出して処理意図を判断するルーティング制御関数です。
「削除」「変更」などの語句を判定して、どの処理(登録・削除・更新)を行うかを振り分けます。
🛠 今回追加されたポイント
今回の拡張では、 askChatgpt() に新たに「削除」「更新」の処理分岐を追加しました。ChatGPTが抽出した予定名や時刻をもとに、 deleteEvent() や updateEvent() 関数を呼び出す構成です。
ただし、「◯◯の予定を削除して」や「変更して」のような曖昧な指示では正しく処理されない場合があるため、予定名+時間の指定が重要になります。
このプロンプト設計が、ChatGPT連携の根幹を支える重要な要素です。
🛠 よくあるエラーと対処法
予定の削除や更新処理を実装する際に直面する可能性のある問題と、その解決方法を紹介します。これらの対応を理解しておくことで、ユーザーにとって信頼性の高いAI執事ボットを構築できます。
🧨 予定が削除できない
LINEで「明日の16時の◯◯の予定を削除して」のように、開始時刻を含めて送信しないと、正確なイベントを特定できず、削除が行われない場合があります。
これは、Googleカレンダー上では同じタイトルの予定が複数存在することがあるため、AI執事ボットが予定名と開始時刻の両方に一致するイベントのみを削除対象としているためです。
- ChatGPTの抽出結果とカレンダー予定が一致しない:
ユーザーが送ったメッセージからChatGPTが抽出した予定名(例:「歯医者」)や開始時刻が、実際のGoogleカレンダー上の予定と完全一致していないと削除されません。 - APIの認証情報が無効:
GoogleカレンダーAPIの認証ファイルが間違っている、またはスコープが不十分な場合、削除処理が失敗します。
🔧 解決方法
- ChatGPTの抽出ロジックで余計な語句を削除し、ユーザーの入力と一致しやすくする
- GoogleカレンダーAPIの認証情報やスコープを再確認する
🔁 予定が正しく更新されない
予定の更新リクエストに対して、何も変化しなかったり、処理が失敗する場合の原因と対策です。
- イベントの削除に失敗している:
updateEvent() では、旧予定を削除してから新しい予定を登録します。削除に失敗すると更新も実行されません。 - ChatGPTが誤ったフォーマットで日時を返す:
抽出された start_time が "2025-04-30 16:00:00" のような文字列である場合、 datetime.strptime() を使って datetime 型に変換しないと登録に失敗します。
🔧 解決方法
- 削除対象の予定(タイトル+時刻)が本当にカレンダー上に存在するか確認する
- ChatGPTから返された start_time を必ず datetime 型に変換してから登録処理に渡す
📌 注意:予定の重複は現在の実装で防止済み
Googleカレンダー自体は、同じ時間帯に複数の予定があってもエラーにはなりませんが、現在のAI執事ボットでは、登録前に重複チェックを行い、同時刻に予定がある場合は登録をブロックするよう実装されています。
そのため、「同じ時間・同じ予定名」の重複登録は発生しないようになっており、ユーザーへも「その時間にはすでに予定が登録されています」というメッセージが返されます。
🎯 プロンプト設計はChatGPT連携の要
いくらChatGPTと連携していても、ユーザーが送る自然文は多種多様です。たとえば「予定を変更したい」「予定を削除して」「〇〇に入れて」など、表現には揺らぎが発生します。
そのため、AI執事ボットの精度を高めるには、「どんな入力文に対して、どのように意図を判定し、予定名と日時を抽出するか」というプロンプト設計こそが中核となります。
今回の実装では、ChatGPTに対して「予定名には余計な語句を含めないこと」「開始時刻は特定の形式で返すこと」など、抽出ルールを明示することで、自然文でも正確に情報が取り出せるように設計しています。
プロンプト設計はChatGPT連携型アプリの“成否を決める”肝であり、テキスト処理系の実装では最優先でチューニングすべき領域です。
🧪 動作検証とテスト
この記事では、削除・更新機能が実際にどのように動作するかを検証するための観点と、テスト例を紹介します。特に、予定の上書き更新、削除の精度、ユーザーへの返信メッセージについて重点的にチェックします。
パターン | 送信内容の例 | すでに同一予定が存在 | 処理の種類 | AIの挙動 | ユーザーへの応答 |
---|---|---|---|---|---|
① 新規登録 | 明日の15時に歯医者 | なし | register | 予定を登録 | ✅ 予定『歯医者』を登録しました。 |
② 同時刻に他の予定あり | 明日の15時に歯医者 | すでに他の予定があり | register | 登録せずエラー返却 | ⚠️ その時間にはすでに予定が登録されています。別の時間を指定してください。 |
③ 同じ予定をもう一度送信 | 明日の15時に歯医者 | すでに「歯医者」が存在 | register | 重複チェックでブロック | ⚠️ その時間にはすでに予定が登録されています。別の時間を指定してください。 |
④ 削除(成功) | 明日の15時の歯医者を削除 | 完全一致の予定が存在 | delete | 削除成功 | ✅ 予定『歯医者』を削除しました。 |
⑤ 削除(失敗) | 明日の15時の歯医者を削除 | 一致する予定が存在しない | delete | 削除されない | 予定『歯医者』は見つかりませんでした。 |
⑥ 更新(成功) | 歯医者の予定を明日の16時に変更 | 旧予定が存在 | update | 旧予定を削除し新規登録 | ✅ 予定『歯医者』を新しい内容で更新しました。 |
⑦ 更新(失敗) | 歯医者の予定を明日の16時に変更 | 該当の旧予定が存在しない | update | 削除対象が見つからず処理中断 | 予定『歯医者』は見つかりませんでした。 |
✅ 検証1:削除機能が正常に動作するか
LINEで「◯◯の予定を削除して」と送信した際、以下を確認します:
- ChatGPTが抽出した予定名と開始時刻がGoogleカレンダー上の予定と完全一致しているか
- 指定範囲(過去30日~未来30日)内で一致するイベントが実際に削除されるか
- 「予定『◯◯』を削除しました」というメッセージが正しく返信されるか
✅ 検証2:更新機能が正常に動作するか
LINEで「◯◯の予定を明日の16時に変更して」と送信した際、以下の確認を行います:
- 該当する旧予定(タイトル+開始時刻の一致)が正しく削除されているか
- 重複チェックを通過した上で、新しい日時で予定が登録されているか
- ChatGPTが返す日時(例:"2025-04-30 16:00:00")が datetime に変換されているか
- 「予定『◯◯』を新しい内容で更新しました」というメッセージが表示されるか
⚠️ 注意:予定の重複は現在の実装で防止済み
Googleカレンダー自体は、同じタイトル・同じ時間の予定が複数存在してもエラーを出しませんが、現在のAI執事ボットでは登録前に同時刻の予定を検索し、重複していれば登録をブロックする実装になっています。
このため、「同じ時間・同じタイトルの予定」を何度送っても、二重登録されることはありません。
💬 ユーザーへのメッセージ確認
削除・更新の成功/失敗、また登録時の重複検知などに応じて、以下のようなLINE返信が行われることを確認してください:
ケース | 返信メッセージ |
---|---|
削除成功 | 予定『◯◯』を削除しました。 |
削除失敗 | 予定『◯◯』は見つかりませんでした。 |
更新成功 | 予定『◯◯』を新しい内容で更新しました。 |
更新失敗 | 更新中にエラーが発生しました。 |
登録済み(重複) | その時間にはすでに予定が登録されています。別の時間を指定してください。 |
まとめと活用方法
この記事では、AI執事ボットに新たに追加された「予定削除・更新」機能の仕組みと実装方法を紹介しました。この機能によって、ユーザーはLINE上の自然なメッセージだけで、Googleカレンダー上の予定を柔軟に管理できるようになります。
🤖 AI執事ボットの実用的な進化
今回の改修により、「予定の通知」にとどまらず、ユーザーからの自然文に基づいた「削除」や「更新」まで対応できるようになりました。特に、ChatGPTを活用して曖昧な命令文(例:「◯時に変更して」)から正確に予定を抽出し、カレンダー操作へとつなげる処理が加わった点が大きな進化です。
📅 活用例:日常的なスケジュール管理にどう使うか?
- 会議の時間変更:「15時からに変更して」とLINEで送るだけで再登録される
- キャンセル対応:「◯◯の予定を削除して」と送れば自動で削除される
- 家庭用タスク調整:子どもの送り迎えや通院予定の入れ替えを手軽に操作
🎯 ユーザー体験の変化と効率化
Googleカレンダーを毎回開かなくても、スマホからLINEに話しかけるだけで予定の追加・変更・削除が完了します。
これにより、技術に不慣れなユーザーでも直感的にスケジュール管理ができるようになり、AIボットが日常の一部として活躍する未来が現実のものとなります。
🔧 今後の拡張に向けて
現在の実装では、イベント名と開始時刻の完全一致を前提とした処理となっていますが、今後は次のような拡張が期待されます:
- タイトルや時間のあいまい一致による柔軟なイベント判定
- 削除・更新時のユーザー確認フロー(例:「◯◯を削除してもよいですか?」)
- ChatGPTによる文脈理解と意図補正による誤処理の軽減
こうした拡張により、より人間的な操作感と信頼性を兼ね備えたAI執事ボットへと進化させることが可能になります。