medical-kiban commited on
Commit
501fbf2
·
1 Parent(s): e686227

カレンダーアップと統合

Browse files
Files changed (2) hide show
  1. app/app.py +12 -3
  2. app/calendar_tool.py +240 -0
app/app.py CHANGED
@@ -6,6 +6,9 @@ from dotenv import load_dotenv
6
  import requests
7
  from typing import Optional
8
  from fastapi import FastAPI, Request, Header, HTTPException
 
 
 
9
  # .env 파일에서 환경 변수를 불러옵니다.
10
  load_dotenv()
11
  app = FastAPI()
@@ -64,11 +67,17 @@ async def handle_chatwork_webhook(
64
  message_id = event.get("message_id")
65
 
66
  message_body = event.get("body")
 
67
  if message_body:
68
  # strip()을 사용하여 앞뒤 공백 및 줄바꿈 문자를 제거하고 출력합니다.
69
  cleaned_message = message_body.strip()
70
- print(f"Received message: {cleaned_message}")
71
- print(f"Received body: {message_body}")
 
 
 
 
 
72
 
73
  if room_id and from_account_id and message_id:
74
  # Chatwork API 엔드포인트
@@ -81,7 +90,7 @@ async def handle_chatwork_webhook(
81
  # [rp] 태그를 사용하면 특정 메시지에 대한 답장 형식으로 보낼 수 있습니다.
82
  reply_body = (
83
  f"[rp aid={from_account_id} to={room_id}-{message_id}]"
84
- "\n정상적으로 수신되었습니다. ✅"
85
  )
86
 
87
  payload = {"body": reply_body}
 
6
  import requests
7
  from typing import Optional
8
  from fastapi import FastAPI, Request, Header, HTTPException
9
+ import sys
10
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
11
+ from calendar_tool import calendar_main
12
  # .env 파일에서 환경 변수를 불러옵니다.
13
  load_dotenv()
14
  app = FastAPI()
 
67
  message_id = event.get("message_id")
68
 
69
  message_body = event.get("body")
70
+ calendar_data = ""
71
  if message_body:
72
  # strip()을 사용하여 앞뒤 공백 및 줄바꿈 문자를 제거하고 출력합니다.
73
  cleaned_message = message_body.strip()
74
+ if cleaned_message[0] != '会':
75
+ print("会議室の検索コマンドではありません。")
76
+ return
77
+ if cleaned_message:
78
+ calendar_data = calendar_main(cleaned_message)
79
+ if not calendar_data:
80
+ return
81
 
82
  if room_id and from_account_id and message_id:
83
  # Chatwork API 엔드포인트
 
90
  # [rp] 태그를 사용하면 특정 메시지에 대한 답장 형식으로 보낼 수 있습니다.
91
  reply_body = (
92
  f"[rp aid={from_account_id} to={room_id}-{message_id}]"
93
+ f"\n✅{calendar_data}"
94
  )
95
 
96
  payload = {"body": reply_body}
app/calendar_tool.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import datetime
4
+ import json
5
+ import pytz
6
+ from google.oauth2 import service_account
7
+ from googleapiclient.discovery import build
8
+ from google.auth.transport.requests import Request
9
+ from google.oauth2.credentials import Credentials
10
+ from google_auth_oauthlib.flow import InstalledAppFlow
11
+ import google.generativeai as genai
12
+ from vertexai.generative_models import GenerativeModel
13
+ from dotenv import load_dotenv
14
+
15
+ load_dotenv()
16
+ base_path = os.path.dirname(__file__)
17
+ credentials_path = os.path.join(base_path, 'new_credential.json')
18
+ token_path = os.path.join(base_path, 'new_token.json')
19
+
20
+ if not os.path.exists(credentials_path):
21
+ print(f"'{credentials_path}' 파일이 없어 새로 생성합니다.")
22
+ credentials_str = os.getenv('GOOGLE_CREDENTIALS_JSON')
23
+ if credentials_str:
24
+ with open(credentials_path, 'w', encoding='utf-8') as f:
25
+ f.write(credentials_str)
26
+ else:
27
+ print("경고: .env 파일에 'GOOGLE_CREDENTIALS_JSON' 변수가 없습니다.")
28
+
29
+ if not os.path.exists(token_path):
30
+ print(f"'{token_path}' 파일이 없어 새로 생성합니다.")
31
+ token_str = os.getenv('GOOGLE_TOKEN_JSON')
32
+ if token_str:
33
+ with open(token_path, 'w', encoding='utf-8') as f:
34
+ f.write(token_str)
35
+
36
+
37
+ CREDENTIALS_FILENAME = credentials_path
38
+ TOKEN_FILENAME = token_path
39
+
40
+ # Google Calendar API 스코프 (읽기 전용)
41
+ SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
42
+ # 기준 시간대 (GAS의 Session.getScriptTimeZone()에 해당)
43
+ # 필요에 따라 'Asia/Tokyo', 'America/New_York' 등으로 변경하세요.
44
+ TIMEZONE = 'Asia/Tokyo'
45
+
46
+ rooms = [
47
+ {"capacity": 6, "name": "1F2", "email": os.getenv("GOOGLE_CALENDAR_1F2")},
48
+ {"capacity": 6, "name": "1F3", "email": os.getenv("GOOGLE_CALENDAR_1F3")},
49
+ {"capacity": 12, "name": "3F1", "email": os.getenv("GOOGLE_CALENDAR_3F1")},
50
+ {"capacity": 4, "name": "3F2", "email": os.getenv("GOOGLE_CALENDAR_3F2")},
51
+ {"capacity": 4, "name": "3F3", "email": os.getenv("GOOGLE_CALENDAR_3F3")},
52
+ {"capacity": 4, "name": "3F5", "email": os.getenv("GOOGLE_CALENDAR_3F5")},
53
+ {"capacity": 4, "name": "3F6", "email": os.getenv("GOOGLE_CALENDAR_3F6")},
54
+ {"capacity": 12, "name": "6F1", "email": os.getenv("GOOGLE_CALENDAR_6F1")},
55
+ {"capacity": 4, "name": "6F2", "email": os.getenv("GOOGLE_CALENDAR_6F2")},
56
+ {"capacity": 4, "name": "6F3", "email": os.getenv("GOOGLE_CALENDAR_6F3")},
57
+ {"capacity": 2, "name": "6F5", "email": os.getenv("GOOGLE_CALENDAR_6F5")},
58
+ {"capacity": 2, "name": "6F6", "email": os.getenv("GOOGLE_CALENDAR_6F6")},
59
+ {"capacity": 2, "name": "6F7", "email": os.getenv("GOOGLE_CALENDAR_6F7")},
60
+ {"capacity": 4, "name": "7F2", "email": os.getenv("GOOGLE_CALENDAR_7F2")},
61
+ {"capacity": 4, "name": "7F3", "email": os.getenv("GOOGLE_CALENDAR_7F3")},
62
+ {"capacity": 4, "name": "7F5", "email": os.getenv("GOOGLE_CALENDAR_7F5") },
63
+ {"capacity": 4, "name": "7F6", "email": os.getenv("GOOGLE_CALENDAR_7F6") }
64
+ ]
65
+
66
+ def get_credentials():
67
+ creds = None
68
+ if os.path.exists(TOKEN_FILENAME):
69
+ creds = Credentials.from_authorized_user_file(TOKEN_FILENAME, SCOPES)
70
+
71
+ # 크리덴셜이 없거나 유효하지 않으면 사용자 로그인 플로우를 실행합니다.
72
+ if not creds or not creds.valid:
73
+ if creds and creds.expired and creds.refresh_token:
74
+ creds.refresh(Request())
75
+ else:
76
+ flow = InstalledAppFlow.from_client_secrets_file(
77
+ CREDENTIALS_FILENAME, SCOPES)
78
+ creds = flow.run_local_server(port=0)
79
+
80
+ # 다음 실행을 위해 토큰을 저장합니다.
81
+ with open(TOKEN_FILENAME, 'w') as token:
82
+ token.write(creds.to_json())
83
+ return creds
84
+
85
+
86
+ def check_room_free_slots_over_1h(date_str=None):
87
+ # 회의실 정보
88
+
89
+ output_lines = []
90
+
91
+ # --- 인증 및 서비스 빌드 ---
92
+ try:
93
+ creds = get_credentials()
94
+ service = build('calendar', 'v3', credentials=creds)
95
+ except Exception as e:
96
+ print(f"認証エラー: {e}")
97
+ return
98
+
99
+ # --- 조회 시간 범위 설정 ---
100
+ # --- 조회 시간 범위 설정 ---
101
+ tz = pytz.timezone(TIMEZONE)
102
+ now = datetime.datetime.now(tz)
103
+
104
+ if date_str:
105
+ target_date = datetime.datetime.strptime(date_str, "%Y-%m-%d").date()
106
+ start_time = tz.localize(datetime.datetime.combine(target_date, datetime.time(10, 0)))
107
+ end_time = tz.localize(datetime.datetime.combine(target_date, datetime.time(19, 0)))
108
+ output_lines.append(f"照会対象: {start_time.strftime('%Y/%m/%d')}")
109
+ else:
110
+ today = now.date()
111
+ start_hour = max(10, now.hour)
112
+ start_time = tz.localize(datetime.datetime.combine(today, datetime.time(start_hour, 0)))
113
+ end_time = tz.localize(datetime.datetime.combine(today, datetime.time(19, 0)))
114
+ output_lines.append(f"照会対象: {start_time.strftime('%Y/%m/%d')}")
115
+
116
+ if start_time >= end_time:
117
+ output_lines.append("照会可能な時間がありません")
118
+ return "\n".join(output_lines)
119
+
120
+ # --- FreeBusy API로 모든 회의실의 바쁜 시간 한 번에 조회 ---
121
+ body = {
122
+ "timeMin": start_time.isoformat(),
123
+ "timeMax": end_time.isoformat(),
124
+ "timeZone": TIMEZONE,
125
+ "items": [{"id": room["email"]} for room in rooms]
126
+ }
127
+
128
+ try:
129
+ freebusy_result = service.freebusy().query(body=body).execute()
130
+ calendars_busy_info = freebusy_result.get('calendars', {})
131
+ except Exception as e:
132
+ return f"FreeBusy API 失敗: {e}"
133
+
134
+ output_lines.append("-" * 30)
135
+
136
+ # --- 각 회의실별로 빈 시간 계산 및 출력 ---
137
+ for room in rooms:
138
+ room_email = room['email']
139
+ busy_info = calendars_busy_info.get(room_email, {})
140
+
141
+ if busy_info.get('errors'):
142
+ reason = busy_info['errors'][0].get('reason', 'unknown')
143
+ output_lines.append(f"[{room['name']}] カレンダー照会失敗: {reason}")
144
+ continue
145
+
146
+ busy_slots = busy_info.get('busy', [])
147
+
148
+ cursor = start_time
149
+ free_slots = []
150
+
151
+ for busy in busy_slots:
152
+ busy_start = datetime.datetime.fromisoformat(busy['start'])
153
+ busy_end = datetime.datetime.fromisoformat(busy['end'])
154
+
155
+ if busy_start > cursor:
156
+ free_slots.append({'start': cursor, 'end': busy_start})
157
+
158
+ if busy_end > cursor:
159
+ cursor = busy_end
160
+
161
+ if cursor < end_time:
162
+ free_slots.append({'start': cursor, 'end': end_time})
163
+
164
+ long_free_slots = [
165
+ slot for slot in free_slots
166
+ if (slot['end'] - slot['start']) >= datetime.timedelta(hours=1)
167
+ ]
168
+
169
+ if long_free_slots:
170
+ output_lines.append(f"[{room['name']}] (定員 {room['capacity']}名)")
171
+ for slot in long_free_slots:
172
+ output_lines.append(f" {slot['start'].strftime('%H:%M')} ~ {slot['end'].strftime('%H:%M')}")
173
+
174
+ return "\n".join(output_lines)
175
+
176
+
177
+ def search_calendar(calendar_data , order: str) -> str:
178
+ genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
179
+
180
+ model = genai.GenerativeModel("gemini-1.5-pro")
181
+
182
+ message = f"""以下が空き室の空いている時間情報です。\n {calendar_data} このデータを見て"""
183
+
184
+
185
+ prompt = "{message}{order}".format(message=message, order=order)
186
+ token_info = model.count_tokens(prompt)
187
+ print(f"Prompt Token Count: {token_info.total_tokens}")
188
+
189
+ response = model.generate_content(prompt)
190
+ return response.text if response.text else "生成失敗"
191
+
192
+
193
+ def calendar_main(input_string: str):
194
+ try:
195
+ parts = input_string.split()
196
+ if len(parts) < 3 or parts[0].lower() != '会議室':
197
+ return
198
+
199
+ # 날짜 파싱 및 포맷팅
200
+ date_input = parts[1]
201
+ formatted_date= ""
202
+ if date_input == "すぐ":
203
+ formatted_date = ""
204
+ elif len(date_input) != 8 or not date_input.isdigit():
205
+ print("日付のフォーマットが正しくありません YYYYMMDD にしてください。")
206
+ return
207
+ else:
208
+ formatted_date = f"{date_input[:4]}-{date_input[4:6]}-{date_input[6:]}"
209
+
210
+ # 인원수 파싱
211
+ capacity_input = parts[2]
212
+ if not capacity_input.isdigit():
213
+ print("人数は数字のみ入力してください。")
214
+ return
215
+ min_capacity = int(capacity_input)
216
+
217
+
218
+ # 4번째 인자가 있으면 취득
219
+ optional_arg = ""
220
+ if len(parts) > 3:
221
+ optional_arg = " ".join(parts[3:])
222
+ print(f"追加オプション: '{optional_arg}'")
223
+
224
+
225
+
226
+
227
+ except Exception as e:
228
+ print(f"エラー発生: {e}")
229
+ return
230
+
231
+ free_rooms = check_room_free_slots_over_1h(formatted_date)
232
+ if free_rooms:
233
+ response = search_calendar(free_rooms, f"{formatted_date}に{min_capacity}名が入れる会議室を教えてください{optional_arg}。回答はシンプルに[会議室名] 定員 空き時間のフォーマットでしてください")
234
+ return response
235
+ else:
236
+ return
237
+
238
+
239
+
240
+