Marco310 commited on
Commit
a7c1db4
·
1 Parent(s): 747f356

Feat: implement input validation and safe transition logic. Added task list checks before planning and fixed concurrency issues on app reset.

Browse files
Files changed (3) hide show
  1. app.py +156 -167
  2. services/planner_service.py +194 -82
  3. src/infra/poi_repository.py +6 -3
app.py CHANGED
@@ -34,14 +34,10 @@ class LifeFlowAI:
34
  self.service = PlannerService()
35
 
36
  def _check_api_status(self, session: UserSession) -> str:
37
- """檢查 API Key 狀態並回傳對應的 Status 文字"""
38
- # 1. 檢查 Session 中的自定義設定
39
  has_gemini = bool(session.custom_settings.get("gemini_api_key"))
40
  has_google = bool(session.custom_settings.get("google_maps_api_key"))
41
 
42
- # 2. 如果沒有自定義,也可以檢查系統預設 (可選)
43
- # 這裡假設我們強制要求用戶輸入或確認
44
-
45
  if has_gemini and has_google:
46
  return "✅ System Ready - Waiting for input..."
47
  else:
@@ -51,106 +47,116 @@ class LifeFlowAI:
51
  return f"⚠️ Missing Keys: {', '.join(missing)} - Please configure Settings ↗"
52
 
53
  def _get_agent_outputs(self, active_agent: str = None, status: str = "idle", message: str = "Waiting") -> List[str]:
54
- """
55
- 輔助函數:生成 6 個 Agent 卡片的 HTML 列表
56
- 用於在 Gradio 中更新 agent_displays
57
- """
58
  agents = ['planner', 'scout', 'optimizer', 'validator', 'weather', 'traffic']
59
  outputs = []
60
  for agent in agents:
61
  if agent == active_agent:
62
  outputs.append(create_agent_card_enhanced(agent, status, message))
63
  else:
64
- # 簡單處理:非活動 Agent 顯示 Idle 或保持原狀 (這裡簡化為 Idle,可根據需求優化)
65
  outputs.append(create_agent_card_enhanced(agent, "idle", "On standby"))
66
  return outputs
67
 
 
 
 
68
  def analyze_wrapper(self, user_input, auto_loc, lat, lon, session_data):
69
- """Step 1: 呼叫 Service 並將結果轉換為 Gradio Updates"""
70
  session = UserSession.from_dict(session_data)
71
 
72
- # 呼叫 Service Generator
 
 
 
 
 
 
 
 
 
 
 
 
73
  iterator = self.service.run_step1_analysis(user_input, auto_loc, lat, lon, session)
74
 
75
  for event in iterator:
76
  evt_type = event.get("type")
77
-
78
- # 準備 Agent 狀態
79
  agent_status = event.get("agent_status", ("planner", "idle", "Waiting"))
80
  agent_outputs = self._get_agent_outputs(*agent_status)
81
-
82
- # 獲取推理過程 HTML
83
- # 注意:event 中可能已更新 session,從 session 獲取最新 reasoning
84
  current_session = event.get("session", session)
85
  reasoning_html = get_reasoning_html_reversed(current_session.reasoning_messages)
86
 
87
  if evt_type == "stream":
 
88
  yield (
89
  create_agent_stream_output().replace("Ready to analyze...", event.get("stream_text", "")),
90
- gr.HTML(), # Task Summary (Empty)
91
- gr.HTML(), # Task List (Empty)
92
- reasoning_html,
93
- gr.update(visible=False), # Confirm Area
94
- gr.update(visible=False), # Chat Input
95
- gr.HTML(), # Chat History
96
- f"Processing: {agent_status[2]}", # Status Bar
97
- *agent_outputs,
98
- current_session.to_dict()
99
  )
100
-
101
  elif evt_type == "complete":
102
- # 完成時生成 Task List HTML
103
  task_html = self.service.generate_task_list_html(current_session)
104
- # 生成 Summary HTML (這裡簡化調用,實際可從 service 獲取)
105
  summary_html = f"<div class='summary-card'>Found {len(current_session.task_list)} tasks</div>"
106
-
107
- # 更新所有 Agent 為完成
108
  final_agents = self._get_agent_outputs("planner", "complete", "Tasks ready")
109
-
110
  yield (
111
  create_agent_stream_output().replace("Ready...", event.get("stream_text", "")),
112
- gr.HTML(value=summary_html),
113
- gr.HTML(value=task_html),
114
- reasoning_html,
115
- gr.update(visible=True), # Confirm Area Show
116
- gr.update(visible=False),
117
  generate_chat_history_html_bubble(current_session),
118
- "✓ Tasks extracted",
119
- *final_agents,
120
- current_session.to_dict()
121
  )
122
-
123
  elif evt_type == "error":
124
  err_msg = event.get("message", "Unknown error")
125
  error_agents = self._get_agent_outputs("planner", "idle", "Error")
126
  yield (
127
- f"<div style='color:red'>Error: {err_msg}</div>",
128
- gr.HTML(), gr.HTML(), reasoning_html,
129
  gr.update(visible=False), gr.update(visible=False), gr.HTML(),
130
- f"Error: {err_msg}",
131
- *error_agents,
132
- current_session.to_dict()
133
  )
134
 
 
 
 
135
  def chat_wrapper(self, msg, session_data):
136
- """Chat Logic Wrapper"""
137
  session = UserSession.from_dict(session_data)
138
  iterator = self.service.modify_task_chat(msg, session)
139
-
140
  for event in iterator:
141
  current_session = event.get("session", session)
142
- chat_html = generate_chat_history_html_bubble(current_session)
143
- task_html = self.service.generate_task_list_html(current_session)
144
-
145
  yield (
146
- chat_html,
147
- task_html,
148
  current_session.to_dict()
149
  )
150
 
151
- def step2_wrapper(self, session_data):
152
- """Step 2 Wrapper"""
 
 
 
 
 
 
 
 
153
  session = UserSession.from_dict(session_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  result = self.service.run_step2_search(session)
155
  current_session = result.get("session", session)
156
 
@@ -158,131 +164,119 @@ class LifeFlowAI:
158
  agent_outputs = self._get_agent_outputs("scout", "working", "Searching POIs...")
159
 
160
  return (
 
 
 
 
161
  reasoning_html,
162
- "🗺️ Scout is searching...",
163
  *agent_outputs,
164
  current_session.to_dict()
165
  )
166
 
 
 
 
167
  def step3_wrapper(self, session_data):
168
- """Step 3 Wrapper"""
169
  session = UserSession.from_dict(session_data)
170
- iterator = self.service.run_step3_team(session)
 
 
171
 
 
172
  for event in iterator:
173
  current_session = event.get("session", session)
174
  if event["type"] == "complete":
175
  yield (event.get("report_html", ""), current_session.to_dict())
176
  elif event["type"] == "error":
177
  yield (f"Error: {event.get('message')}", current_session.to_dict())
178
- # 可以根據需要處理中間狀態更新
179
 
 
 
 
180
  def step4_wrapper(self, session_data):
181
- """Step 4 Wrapper"""
182
  session = UserSession.from_dict(session_data)
 
 
 
 
 
 
 
 
 
183
  result = self.service.run_step4_finalize(session)
184
  current_session = result.get("session", session)
185
 
186
  if result["type"] == "success":
187
  agent_outputs = self._get_agent_outputs("team", "complete", "Done")
188
  return (
189
- result["timeline_html"],
190
- result["metrics_html"],
191
- result["result_html"],
192
- result["map_fig"],
193
- gr.update(visible=True), # Show Map Tab
194
- gr.update(visible=False), # Hide Team Area
195
- "🎉 Planning completed!",
196
- *agent_outputs,
197
- current_session.to_dict()
198
  )
199
  else:
200
- # Error handling
201
  default_map = create_animated_map()
202
  agent_outputs = self._get_agent_outputs("team", "idle", "Error")
203
  err = result.get("message", "Error")
204
  return (
205
  f"Error: {err}", "", "", default_map,
206
- gr.update(), gr.update(),
207
- f"Error: {err}",
208
- *agent_outputs,
209
- current_session.to_dict()
210
  )
211
 
212
  def save_settings(self, google_key, weather_key, gemini_key, model, session_data):
213
- """Settings Save Wrapper"""
214
  session = UserSession.from_dict(session_data)
215
  session.custom_settings['google_maps_api_key'] = google_key
216
  session.custom_settings['openweather_api_key'] = weather_key
217
  session.custom_settings['gemini_api_key'] = gemini_key
218
  session.custom_settings['model'] = model
219
-
220
  new_status = self._check_api_status(session)
221
-
222
  return "✅ Settings saved locally!", session.to_dict(), new_status
223
 
 
 
 
224
  def build_interface(self):
225
  def reset_app(old_session_data):
226
- # 1. 讀取舊 Session
227
  old_session = UserSession.from_dict(old_session_data)
228
-
229
- # 2. 創建新 Session (數據歸零)
230
  new_session = UserSession()
231
-
232
- # 3. 🔥 關鍵:繼承舊的設定 (API Keys, Model)
233
  new_session.custom_settings = old_session.custom_settings
234
-
235
  status_msg = self._check_api_status(new_session)
236
-
237
- # 4. 回傳重置後的 UI 狀態
238
  return (
239
- gr.update(visible=True), gr.update(visible=False), # Input Show, Confirm Hide
240
- gr.update(visible=False), gr.update(visible=False), # Chat Hide, Result Hide
241
- gr.update(visible=False), gr.update(visible=False), # Team Hide, Report Hide
242
- gr.update(visible=False), "", # Map Hide, Textbox Clear
243
  create_agent_stream_output(),
244
  status_msg,
245
- new_session.to_dict() # Reset Session but keep keys
 
246
  )
247
 
248
  def initial_check(session_data):
249
  s = UserSession.from_dict(session_data)
250
- # 如果環境變數有值,可以在這裡預先填入 custom_settings (視需求而定)
251
  return self._check_api_status(s)
252
 
253
  with gr.Blocks(title=APP_TITLE) as demo:
254
  gr.HTML(get_enhanced_css())
255
- #create_header()
256
- #theme_btn, settings_btn, doc_btn = create_top_controls()
257
  home_btn, theme_btn, settings_btn, doc_btn = create_header()
258
-
259
- # State
260
  session_state = gr.State(value=UserSession().to_dict())
261
 
262
-
263
  with gr.Row():
264
- # Left Column
265
  with gr.Column(scale=2, min_width=400):
266
  (input_area, agent_stream_output, user_input, auto_location,
267
  location_inputs, lat_input, lon_input, analyze_btn) = create_input_form(
268
  create_agent_stream_output()
269
  )
270
-
271
  (task_confirm_area, task_summary_display,
272
  task_list_display, exit_btn_inline, ready_plan_btn) = create_confirmation_area()
273
-
274
- # Team Area
275
  team_area, agent_displays = create_team_area(create_agent_card_enhanced)
276
-
277
- # Result Area
278
  (result_area, result_display, timeline_display, metrics_display) = create_result_area(
279
  create_animated_map)
280
 
281
- # Right Column
282
  with gr.Column(scale=3, min_width=500):
283
- status_bar = gr.Textbox(label="📊 Status",
284
- value="⚠️ Checking System Status...",
285
- interactive=False,
286
  max_lines=1)
287
  (tabs, report_tab, map_tab, report_output, map_output, reasoning_output,
288
  chat_input_area, chat_history_output, chat_input, chat_send) = create_tabs(
@@ -291,75 +285,59 @@ class LifeFlowAI:
291
 
292
  # Modals
293
  (settings_modal, google_maps_key, openweather_key, gemini_api_key,
294
- model_choice, close_settings_btn, save_settings_btn,
295
- settings_status) = create_settings_modal()
296
  doc_modal, close_doc_btn = create_doc_modal()
297
 
298
- # ===== Event Binding =====
299
  auto_location.change(fn=toggle_location_inputs, inputs=[auto_location], outputs=[location_inputs])
300
 
301
- # Step 1: Analyze
302
- analyze_btn.click(
 
 
303
  fn=self.analyze_wrapper,
304
  inputs=[user_input, auto_location, lat_input, lon_input, session_state],
305
  outputs=[
306
  agent_stream_output, task_summary_display, task_list_display,
307
  reasoning_output, task_confirm_area, chat_input_area,
308
- chat_history_output, status_bar, *agent_displays, session_state
 
309
  ]
310
- ).then(
311
- fn=lambda: (gr.update(visible=False), gr.update(visible=True), gr.update(visible=True)),
312
- outputs=[input_area, task_confirm_area, chat_input_area]
313
  )
314
 
315
- # Chat
316
- chat_send.click(
 
 
317
  fn=self.chat_wrapper,
318
  inputs=[chat_input, session_state],
319
  outputs=[chat_history_output, task_list_display, session_state]
320
  ).then(fn=lambda: "", outputs=[chat_input])
321
 
322
- # 更新 Home 鍵綁定
323
- home_btn.click(
324
- fn=reset_app,
 
 
 
325
  inputs=[session_state],
326
  outputs=[
327
- input_area, task_confirm_area, chat_input_area, result_area,
328
- team_area, report_tab, map_tab, user_input,
329
- agent_stream_output, status_bar, session_state # 確保 status_bar 在列表裡
330
  ]
331
  )
332
 
333
- # 更新 Exit 鍵綁定 (同上)
334
- exit_btn_inline.click(
335
- fn=reset_app,
336
- inputs=[session_state],
337
- outputs=[
338
- input_area, task_confirm_area, chat_input_area, result_area,
339
- team_area, report_tab, map_tab, user_input,
340
- agent_stream_output, status_bar, session_state
341
- ]
342
- )
343
-
344
-
345
-
346
- # Step 2, 3, 4 Sequence
347
- ready_plan_btn.click(
348
- fn=lambda: (gr.update(visible=False), gr.update(visible=False),
349
- gr.update(visible=True), gr.update(selected="ai_conversation_tab")),
350
- outputs=[task_confirm_area, chat_input_area, team_area, tabs]
351
- ).then(
352
- fn=self.step2_wrapper,
353
- inputs=[session_state],
354
- outputs=[reasoning_output, status_bar, *agent_displays, session_state]
355
- ).then(
356
  fn=self.step3_wrapper,
357
  inputs=[session_state],
358
  outputs=[report_output, session_state]
359
- ).then(
 
 
360
  fn=lambda: (gr.update(visible=True), gr.update(visible=True), gr.update(selected="report_tab")),
361
  outputs=[report_tab, map_tab, tabs]
362
- ).then(
 
 
363
  fn=self.step4_wrapper,
364
  inputs=[session_state],
365
  outputs=[
@@ -368,33 +346,44 @@ class LifeFlowAI:
368
  ]
369
  ).then(fn=lambda: gr.update(visible=True), outputs=[result_area])
370
 
371
- # Settings & Docs Handlers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  settings_btn.click(fn=lambda: gr.update(visible=True), outputs=[settings_modal])
373
  close_settings_btn.click(fn=lambda: gr.update(visible=False), outputs=[settings_modal])
374
-
375
  save_settings_btn.click(
376
  fn=self.save_settings,
377
  inputs=[google_maps_key, openweather_key, gemini_api_key, model_choice, session_state],
378
- outputs=[settings_status, session_state, status_bar] # 增加 status_bar
379
  )
380
-
381
- theme_btn.click(fn=None, js="""
382
- () => {
383
- const container = document.querySelector('.gradio-container');
384
- if (container) {
385
- container.classList.toggle('theme-dark');
386
- const isDark = container.classList.contains('theme-dark');
387
- localStorage.setItem('lifeflow-theme', isDark ? 'dark' : 'light');
388
- }
389
- }
390
- """)
391
-
392
  doc_btn.click(fn=lambda: gr.update(visible=True), outputs=[doc_modal])
393
  close_doc_btn.click(fn=lambda: gr.update(visible=False), outputs=[doc_modal])
394
-
395
- demo.load(fn=initial_check,
396
- inputs=[session_state],
397
- outputs=[status_bar])
398
  return demo
399
 
400
 
 
34
  self.service = PlannerService()
35
 
36
  def _check_api_status(self, session: UserSession) -> str:
37
+ """檢查 API Key 狀態"""
 
38
  has_gemini = bool(session.custom_settings.get("gemini_api_key"))
39
  has_google = bool(session.custom_settings.get("google_maps_api_key"))
40
 
 
 
 
41
  if has_gemini and has_google:
42
  return "✅ System Ready - Waiting for input..."
43
  else:
 
47
  return f"⚠️ Missing Keys: {', '.join(missing)} - Please configure Settings ↗"
48
 
49
  def _get_agent_outputs(self, active_agent: str = None, status: str = "idle", message: str = "Waiting") -> List[str]:
50
+ """輔助函數:生成 Agent 卡片 HTML"""
 
 
 
51
  agents = ['planner', 'scout', 'optimizer', 'validator', 'weather', 'traffic']
52
  outputs = []
53
  for agent in agents:
54
  if agent == active_agent:
55
  outputs.append(create_agent_card_enhanced(agent, status, message))
56
  else:
 
57
  outputs.append(create_agent_card_enhanced(agent, "idle", "On standby"))
58
  return outputs
59
 
60
+ # -------------------------------------------------------------------------
61
+ # Step 1: Analyze (分析任務)
62
+ # -------------------------------------------------------------------------
63
  def analyze_wrapper(self, user_input, auto_loc, lat, lon, session_data):
 
64
  session = UserSession.from_dict(session_data)
65
 
66
+ # 1. 輸入驗證
67
+ if not user_input or not user_input.strip():
68
+ agent_outputs = self._get_agent_outputs("planner", "idle", "Waiting")
69
+ yield (
70
+ "<div style='color: #ef4444; font-weight: bold; padding: 5px;'>⚠️ Please describe your plans first!</div>",
71
+ gr.HTML(), gr.HTML(),
72
+ get_reasoning_html_reversed(session.reasoning_messages),
73
+ gr.update(visible=False), gr.update(visible=False), gr.HTML(),
74
+ "⚠️ Input Required", gr.update(visible=True), *agent_outputs, session.to_dict()
75
+ )
76
+ return
77
+
78
+ # 2. 執行分析
79
  iterator = self.service.run_step1_analysis(user_input, auto_loc, lat, lon, session)
80
 
81
  for event in iterator:
82
  evt_type = event.get("type")
 
 
83
  agent_status = event.get("agent_status", ("planner", "idle", "Waiting"))
84
  agent_outputs = self._get_agent_outputs(*agent_status)
 
 
 
85
  current_session = event.get("session", session)
86
  reasoning_html = get_reasoning_html_reversed(current_session.reasoning_messages)
87
 
88
  if evt_type == "stream":
89
+ # Streaming: 保持 Input 顯示,Confirm 隱藏
90
  yield (
91
  create_agent_stream_output().replace("Ready to analyze...", event.get("stream_text", "")),
92
+ gr.HTML(), gr.HTML(), reasoning_html,
93
+ gr.update(visible=False), gr.update(visible=False), gr.HTML(),
94
+ f"Processing: {agent_status[2]}", gr.update(visible=True), *agent_outputs, current_session.to_dict()
 
 
 
 
 
 
95
  )
 
96
  elif evt_type == "complete":
97
+ # Complete: 切換到 Confirm 頁面
98
  task_html = self.service.generate_task_list_html(current_session)
 
99
  summary_html = f"<div class='summary-card'>Found {len(current_session.task_list)} tasks</div>"
 
 
100
  final_agents = self._get_agent_outputs("planner", "complete", "Tasks ready")
 
101
  yield (
102
  create_agent_stream_output().replace("Ready...", event.get("stream_text", "")),
103
+ gr.HTML(value=summary_html), gr.HTML(value=task_html), reasoning_html,
104
+ gr.update(visible=True), gr.update(visible=True),
 
 
 
105
  generate_chat_history_html_bubble(current_session),
106
+ "✓ Tasks extracted", gr.update(visible=False), *final_agents, current_session.to_dict()
 
 
107
  )
 
108
  elif evt_type == "error":
109
  err_msg = event.get("message", "Unknown error")
110
  error_agents = self._get_agent_outputs("planner", "idle", "Error")
111
  yield (
112
+ f"<div style='color:red'>Error: {err_msg}</div>", gr.HTML(), gr.HTML(), reasoning_html,
 
113
  gr.update(visible=False), gr.update(visible=False), gr.HTML(),
114
+ f"Error: {err_msg}", gr.update(visible=True), *error_agents, current_session.to_dict()
 
 
115
  )
116
 
117
+ # -------------------------------------------------------------------------
118
+ # Chat Modification (修改任務)
119
+ # -------------------------------------------------------------------------
120
  def chat_wrapper(self, msg, session_data):
 
121
  session = UserSession.from_dict(session_data)
122
  iterator = self.service.modify_task_chat(msg, session)
 
123
  for event in iterator:
124
  current_session = event.get("session", session)
 
 
 
125
  yield (
126
+ generate_chat_history_html_bubble(current_session),
127
+ self.service.generate_task_list_html(current_session),
128
  current_session.to_dict()
129
  )
130
 
131
+ # -------------------------------------------------------------------------
132
+ # Step 2 -> 3 Transition (驗證與過渡)
133
+ # -------------------------------------------------------------------------
134
+ def transition_to_planning(self, session_data):
135
+ """
136
+ Step 2 -> Step 3 過渡函數:
137
+ 1. 檢查是否有任務 (防呆)
138
+ 2. 若無任務:報錯並留在原位
139
+ 3. 若有任務:隱藏 Confirm Area,顯示 Team Area,並執行 Step 2 (Search)
140
+ """
141
  session = UserSession.from_dict(session_data)
142
+
143
+ # 1. 🛑 防呆檢查
144
+ if not session.task_list or len(session.task_list) == 0:
145
+ agent_outputs = self._get_agent_outputs("planner", "idle", "No tasks")
146
+ reasoning = get_reasoning_html_reversed(session.reasoning_messages)
147
+
148
+ return (
149
+ gr.update(visible=True), # task_confirm_area (保持顯示)
150
+ gr.update(visible=True), # chat_input_area
151
+ gr.update(visible=False), # team_area
152
+ gr.update(), # tabs
153
+ reasoning,
154
+ "⚠️ Error: No tasks to plan! Please add tasks.", # Status Bar
155
+ *agent_outputs,
156
+ session.to_dict()
157
+ )
158
+
159
+ # 2. ✅ 執行 Step 2 (Scout Search)
160
  result = self.service.run_step2_search(session)
161
  current_session = result.get("session", session)
162
 
 
164
  agent_outputs = self._get_agent_outputs("scout", "working", "Searching POIs...")
165
 
166
  return (
167
+ gr.update(visible=False), # task_confirm_area (隱藏)
168
+ gr.update(visible=False), # chat_input_area
169
+ gr.update(visible=True), # team_area (顯示)
170
+ gr.update(selected="ai_conversation_tab"), # 切換 Tab
171
  reasoning_html,
172
+ "🗺️ Scout is searching locations...",
173
  *agent_outputs,
174
  current_session.to_dict()
175
  )
176
 
177
+ # -------------------------------------------------------------------------
178
+ # Step 3 Wrapper (Team Analysis)
179
+ # -------------------------------------------------------------------------
180
  def step3_wrapper(self, session_data):
 
181
  session = UserSession.from_dict(session_data)
182
+ if not session.task_list: # 二次防呆
183
+ yield ("", session.to_dict())
184
+ return
185
 
186
+ iterator = self.service.run_step3_team(session)
187
  for event in iterator:
188
  current_session = event.get("session", session)
189
  if event["type"] == "complete":
190
  yield (event.get("report_html", ""), current_session.to_dict())
191
  elif event["type"] == "error":
192
  yield (f"Error: {event.get('message')}", current_session.to_dict())
 
193
 
194
+ # -------------------------------------------------------------------------
195
+ # Step 4 Wrapper (Optimization & Results)
196
+ # -------------------------------------------------------------------------
197
  def step4_wrapper(self, session_data):
 
198
  session = UserSession.from_dict(session_data)
199
+ if not session.task_list: # 二次防呆
200
+ default_map = create_animated_map()
201
+ agent_outputs = self._get_agent_outputs("planner", "idle", "No tasks")
202
+ return (
203
+ "", "", "", default_map,
204
+ gr.update(), gr.update(), "⚠️ Planning aborted",
205
+ *agent_outputs, session.to_dict()
206
+ )
207
+
208
  result = self.service.run_step4_finalize(session)
209
  current_session = result.get("session", session)
210
 
211
  if result["type"] == "success":
212
  agent_outputs = self._get_agent_outputs("team", "complete", "Done")
213
  return (
214
+ result["timeline_html"], result["metrics_html"], result["result_html"], result["map_fig"],
215
+ gr.update(visible=True), gr.update(visible=False), "🎉 Planning completed!",
216
+ *agent_outputs, current_session.to_dict()
 
 
 
 
 
 
217
  )
218
  else:
 
219
  default_map = create_animated_map()
220
  agent_outputs = self._get_agent_outputs("team", "idle", "Error")
221
  err = result.get("message", "Error")
222
  return (
223
  f"Error: {err}", "", "", default_map,
224
+ gr.update(), gr.update(), f"Error: {err}",
225
+ *agent_outputs, current_session.to_dict()
 
 
226
  )
227
 
228
  def save_settings(self, google_key, weather_key, gemini_key, model, session_data):
 
229
  session = UserSession.from_dict(session_data)
230
  session.custom_settings['google_maps_api_key'] = google_key
231
  session.custom_settings['openweather_api_key'] = weather_key
232
  session.custom_settings['gemini_api_key'] = gemini_key
233
  session.custom_settings['model'] = model
 
234
  new_status = self._check_api_status(session)
 
235
  return "✅ Settings saved locally!", session.to_dict(), new_status
236
 
237
+ # -------------------------------------------------------------------------
238
+ # UI Builder
239
+ # -------------------------------------------------------------------------
240
  def build_interface(self):
241
  def reset_app(old_session_data):
 
242
  old_session = UserSession.from_dict(old_session_data)
 
 
243
  new_session = UserSession()
 
 
244
  new_session.custom_settings = old_session.custom_settings
 
245
  status_msg = self._check_api_status(new_session)
 
 
246
  return (
247
+ gr.update(visible=True), gr.update(visible=False),
248
+ gr.update(visible=False), gr.update(visible=False),
249
+ gr.update(visible=False), gr.update(visible=False),
250
+ gr.update(visible=False), "",
251
  create_agent_stream_output(),
252
  status_msg,
253
+ "", "", "",
254
+ new_session.to_dict()
255
  )
256
 
257
  def initial_check(session_data):
258
  s = UserSession.from_dict(session_data)
 
259
  return self._check_api_status(s)
260
 
261
  with gr.Blocks(title=APP_TITLE) as demo:
262
  gr.HTML(get_enhanced_css())
 
 
263
  home_btn, theme_btn, settings_btn, doc_btn = create_header()
 
 
264
  session_state = gr.State(value=UserSession().to_dict())
265
 
 
266
  with gr.Row():
 
267
  with gr.Column(scale=2, min_width=400):
268
  (input_area, agent_stream_output, user_input, auto_location,
269
  location_inputs, lat_input, lon_input, analyze_btn) = create_input_form(
270
  create_agent_stream_output()
271
  )
 
272
  (task_confirm_area, task_summary_display,
273
  task_list_display, exit_btn_inline, ready_plan_btn) = create_confirmation_area()
 
 
274
  team_area, agent_displays = create_team_area(create_agent_card_enhanced)
 
 
275
  (result_area, result_display, timeline_display, metrics_display) = create_result_area(
276
  create_animated_map)
277
 
 
278
  with gr.Column(scale=3, min_width=500):
279
+ status_bar = gr.Textbox(label="📊 Status", value="⚠️ Checking System Status...", interactive=False,
 
 
280
  max_lines=1)
281
  (tabs, report_tab, map_tab, report_output, map_output, reasoning_output,
282
  chat_input_area, chat_history_output, chat_input, chat_send) = create_tabs(
 
285
 
286
  # Modals
287
  (settings_modal, google_maps_key, openweather_key, gemini_api_key,
288
+ model_choice, close_settings_btn, save_settings_btn, settings_status) = create_settings_modal()
 
289
  doc_modal, close_doc_btn = create_doc_modal()
290
 
 
291
  auto_location.change(fn=toggle_location_inputs, inputs=[auto_location], outputs=[location_inputs])
292
 
293
+ # ---------------------------------------------------------------------
294
+ # Event 1: Analyze (Step 1)
295
+ # ---------------------------------------------------------------------
296
+ analyze_event = analyze_btn.click(
297
  fn=self.analyze_wrapper,
298
  inputs=[user_input, auto_location, lat_input, lon_input, session_state],
299
  outputs=[
300
  agent_stream_output, task_summary_display, task_list_display,
301
  reasoning_output, task_confirm_area, chat_input_area,
302
+ chat_history_output, status_bar, input_area,
303
+ *agent_displays, session_state
304
  ]
 
 
 
305
  )
306
 
307
+ # ---------------------------------------------------------------------
308
+ # Event 2: Chat
309
+ # ---------------------------------------------------------------------
310
+ chat_event = chat_send.click(
311
  fn=self.chat_wrapper,
312
  inputs=[chat_input, session_state],
313
  outputs=[chat_history_output, task_list_display, session_state]
314
  ).then(fn=lambda: "", outputs=[chat_input])
315
 
316
+ # ---------------------------------------------------------------------
317
+ # Event 3: Planning Chain (Step 2 -> 3 -> 4)
318
+ # 使用 transition_to_planning 進行驗證,如果沒任務會自動停止
319
+ # ---------------------------------------------------------------------
320
+ step2_event = ready_plan_btn.click(
321
+ fn=self.transition_to_planning,
322
  inputs=[session_state],
323
  outputs=[
324
+ task_confirm_area, chat_input_area, team_area, tabs,
325
+ reasoning_output, status_bar, *agent_displays, session_state
 
326
  ]
327
  )
328
 
329
+ step3_event = step2_event.then(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  fn=self.step3_wrapper,
331
  inputs=[session_state],
332
  outputs=[report_output, session_state]
333
+ )
334
+
335
+ step3_ui_update = step3_event.then(
336
  fn=lambda: (gr.update(visible=True), gr.update(visible=True), gr.update(selected="report_tab")),
337
  outputs=[report_tab, map_tab, tabs]
338
+ )
339
+
340
+ step4_event = step3_ui_update.then(
341
  fn=self.step4_wrapper,
342
  inputs=[session_state],
343
  outputs=[
 
346
  ]
347
  ).then(fn=lambda: gr.update(visible=True), outputs=[result_area])
348
 
349
+ # ---------------------------------------------------------------------
350
+ # Reset & Cancels
351
+ # ---------------------------------------------------------------------
352
+ reset_outputs = [
353
+ input_area, task_confirm_area, chat_input_area, result_area,
354
+ team_area, report_tab, map_tab, user_input,
355
+ agent_stream_output, status_bar,
356
+ task_summary_display, task_list_display, chat_history_output,
357
+ session_state
358
+ ]
359
+
360
+ home_btn.click(
361
+ fn=reset_app,
362
+ inputs=[session_state],
363
+ outputs=reset_outputs,
364
+ cancels=[analyze_event, step4_event, chat_event]
365
+ )
366
+
367
+ exit_btn_inline.click(
368
+ fn=reset_app,
369
+ inputs=[session_state],
370
+ outputs=reset_outputs,
371
+ cancels=[analyze_event, step4_event, chat_event]
372
+ )
373
+
374
+ # Others
375
  settings_btn.click(fn=lambda: gr.update(visible=True), outputs=[settings_modal])
376
  close_settings_btn.click(fn=lambda: gr.update(visible=False), outputs=[settings_modal])
 
377
  save_settings_btn.click(
378
  fn=self.save_settings,
379
  inputs=[google_maps_key, openweather_key, gemini_api_key, model_choice, session_state],
380
+ outputs=[settings_status, session_state, status_bar]
381
  )
382
+ theme_btn.click(fn=None,
383
+ js="() => { document.querySelector('.gradio-container').classList.toggle('theme-dark'); }")
 
 
 
 
 
 
 
 
 
 
384
  doc_btn.click(fn=lambda: gr.update(visible=True), outputs=[doc_modal])
385
  close_doc_btn.click(fn=lambda: gr.update(visible=False), outputs=[doc_modal])
386
+ demo.load(fn=initial_check, inputs=[session_state], outputs=[status_bar])
 
 
 
387
  return demo
388
 
389
 
services/planner_service.py CHANGED
@@ -54,106 +54,186 @@ class PlannerService:
54
 
55
  def _get_live_session(self, incoming_session: UserSession) -> UserSession:
56
  """
57
- [Fix] 輔助方法:
58
- 從 Gradio 傳來的 incoming_session 只有數據 (Agent 為 None)。
59
- 我們需要從 _active_sessions 取回包含 Agent 的真實 Session。
60
  """
61
  sid = incoming_session.session_id
62
 
63
- # 如果這個 Session ID 已經在內存中,取出它並同步數據
 
 
 
64
  if sid and sid in self._active_sessions:
65
  live_session = self._active_sessions[sid]
66
- # 同步前端可能修改過的數據到 Live Session
 
67
  live_session.lat = incoming_session.lat
68
  live_session.lng = incoming_session.lng
69
- # 注意:這裡不覆蓋 task_list,除非我們確定前端有修改權限
70
- # 這裡簡單假設前端傳來的 chat history 是最新的
 
 
 
 
 
 
71
  if len(incoming_session.chat_history) > len(live_session.chat_history):
72
  live_session.chat_history = incoming_session.chat_history
 
73
  return live_session
74
 
75
- # 如果是新的 Session,或者內存中沒有,則註冊它
76
- if sid:
77
- self._active_sessions[sid] = incoming_session
78
  return incoming_session
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  def initialize_agents(self, session: UserSession, lat: float, lng: float) -> UserSession:
81
- """初始化 Agents 並保存到內存"""
82
- # 1. 確保我們操作的是 Live Session
83
- session = self._get_live_session(session)
84
 
 
 
85
  session.lat = lat
86
  session.lng = lng
87
 
 
 
 
 
 
88
  if session.planner_agent is not None:
 
89
  return session
90
 
91
- # 生成或恢復 Session ID
92
- if session.session_id is None:
93
- session.session_id = str(uuid.uuid4())
94
- # 註冊到內存
95
- self._active_sessions[session.session_id] = session
96
- set_session_id(session.session_id)
97
- logger.info(f"🆔 New Session: {session.session_id}")
98
- else:
99
- set_session_id(session.session_id)
100
- logger.info(f"🔄 Restoring Session: {session.session_id}")
101
 
102
- # 設定用戶狀態
103
- session.user_state = UserState(location=Location(lat=lat, lng=lng))
 
 
104
 
105
- # 讀取設定
106
- selected_model_id = 'gemini-2.5-flash'
107
- gemini_api_key = session.custom_settings.get("gemini_api_key", "")
108
- google_maps_api_key = session.custom_settings.get("google_maps_api_key", "")
109
- openweather_api_key = session.custom_settings.get("openweather_api_key", "")
110
 
111
- # 初始化模型
112
- planner_model = Gemini(id=selected_model_id, thinking_budget=2048, api_key=gemini_api_key)
113
- main_model = Gemini(id=selected_model_id, thinking_budget=1024, api_key=gemini_api_key)
114
- lite_model = Gemini(id="gemini-2.5-flash-lite", api_key=gemini_api_key)
115
 
116
  models_dict = {
117
  "team": main_model, "scout": main_model, "optimizer": lite_model,
118
  "navigator": lite_model, "weatherman": lite_model, "presenter": main_model,
119
  }
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  tools_dict = {
122
- "scout": [ScoutToolkit(google_maps_api_key)],
123
- "optimizer": [OptimizationToolkit(google_maps_api_key)],
124
- "navigator": [NavigationToolkit(google_maps_api_key)],
125
- "weatherman": [WeatherToolkit(openweather_api_key)],
126
- "presenter": [ReaderToolkit()],
127
  }
128
 
129
  planner_kwargs = {
130
- "additional_context": get_context(session.user_state),
131
  "timezone_identifier": session.user_state.utc_offset,
132
  "debug_mode": False,
133
  }
134
 
135
  team_kwargs = {"timezone_identifier": session.user_state.utc_offset}
136
 
137
- # 創建 Agents
138
  session.planner_agent = create_planner_agent(planner_model, planner_kwargs, session_id=session.session_id)
139
  session.core_team = create_core_team(models_dict, team_kwargs, tools_dict, session_id=session.session_id)
140
 
 
 
141
  logger.info(f"✅ Agents initialized for session {session.session_id}")
142
  return session
143
 
144
  # ================= Step 1: Analyze Tasks =================
145
 
146
  def run_step1_analysis(self, user_input: str, auto_location: bool,
147
- lat: float, lon: float, session: UserSession) -> Generator[Dict[str, Any], None, None]:
148
- if not user_input.strip():
149
- yield {"type": "empty"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  return
151
 
152
- if auto_location:
153
- lat, lon = 25.033, 121.565
 
 
 
 
 
 
154
 
155
  try:
156
- # 🌟 [Fix] 使用 Live Session
157
  session = self.initialize_agents(session, lat, lon)
158
 
159
  # 階段 1: 初始化
@@ -164,19 +244,12 @@ class PlannerService:
164
  "agent_status": ("planner", "working", "Initializing..."),
165
  "session": session
166
  }
167
- time.sleep(0.3)
168
 
169
- # 階段 2: 提取任務
170
  self._add_reasoning(session, "planner", f"Processing: {user_input[:50]}...")
171
  current_text = "🤔 Analyzing your request with AI...\n📋 AI is extracting tasks..."
172
- yield {
173
- "type": "stream",
174
- "stream_text": current_text,
175
- "agent_status": ("planner", "working", "Extracting tasks..."),
176
- "session": session
177
- }
178
 
179
- # 呼叫 Agent (Stream)
180
  planner_stream = session.planner_agent.run(
181
  f"help user to update the task_list, user's message: {user_input}",
182
  stream=True, stream_events=True
@@ -189,12 +262,13 @@ class PlannerService:
189
  if chunk.event == RunEvent.run_content:
190
  content = chunk.content
191
  accumulated_response += content
192
- if "@@@" not in accumulated_response:
193
  displayed_text += content
 
194
  yield {
195
  "type": "stream",
196
  "stream_text": displayed_text,
197
- "agent_status": ("planner", "working", "Processing..."),
198
  "session": session
199
  }
200
 
@@ -214,33 +288,49 @@ class PlannerService:
214
  logger.error(f"Failed to parse task_list: {e}")
215
  session.task_list = []
216
 
217
- self._add_reasoning(session, "planner", f"✅ Extracted {len(session.task_list)} tasks")
218
-
219
- high_priority = sum(1 for t in session.task_list if t.get("priority") == "HIGH")
220
- total_time = sum(int(t.get("duration", "0").split()[0]) for t in session.task_list if t.get("duration"))
221
-
 
 
 
 
 
 
 
 
 
222
  yield {
223
  "type": "complete",
224
- "stream_text": displayed_text + f"\n✅ Analysis complete! Found {len(session.task_list)} tasks.",
225
- "high_priority": high_priority,
226
- "total_time": total_time,
227
- "session": session
 
228
  }
229
 
230
  except Exception as e:
231
- logger.error(f"Error in step1: {e}", exc_info=True)
232
- yield {"type": "error", "message": str(e), "session": session}
233
 
234
  # ================= Task Modification (Chat) =================
235
 
236
  def modify_task_chat(self, user_message: str, session: UserSession) -> Generator[Dict[str, Any], None, None]:
237
- if not user_message.strip():
238
- yield {"type": "no_change", "session": session}
 
 
 
 
 
 
239
  return
240
 
241
- # 🌟 [Fix] 獲取 Live Session
242
  session = self._get_live_session(session)
243
 
 
244
  session.chat_history.append({
245
  "role": "user", "message": user_message, "time": datetime.now().strftime("%H:%M:%S")
246
  })
@@ -248,13 +338,13 @@ class PlannerService:
248
 
249
  try:
250
  if session.planner_agent is None:
251
- # 嘗試重新初始化 (如果遺失)
252
  if session.lat and session.lng:
253
  session = self.initialize_agents(session, session.lat, session.lng)
254
  else:
255
- yield {"type": "error", "message": "Session lost. Please restart.", "session": session}
256
  return
257
 
 
258
  session.chat_history.append({
259
  "role": "assistant", "message": "🤔 AI is thinking...", "time": datetime.now().strftime("%H:%M:%S")
260
  })
@@ -270,6 +360,7 @@ class PlannerService:
270
  if chunk.event == RunEvent.run_content:
271
  content = chunk.content
272
  accumulated_response += content
 
273
 
274
  json_data = "{" + accumulated_response.split("{", maxsplit=1)[-1]
275
  json_data = json_data.replace("`", "").replace("@", "").replace("\\", " ").replace("\n", " ")
@@ -282,6 +373,11 @@ class PlannerService:
282
  task_list_data = json.loads(json_data)
283
  session.task_list = self._convert_task_list_to_ui_format(task_list_data)
284
 
 
 
 
 
 
285
  session.chat_history[-1] = {
286
  "role": "assistant",
287
  "message": "✅ Tasks updated based on your request",
@@ -289,20 +385,27 @@ class PlannerService:
289
  }
290
  self._add_reasoning(session, "planner", f"Updated: {user_message[:30]}...")
291
 
292
- yield {"type": "complete", "session": session}
 
 
 
 
293
 
294
  except Exception as e:
295
  logger.error(f"Chat error: {e}")
296
  session.chat_history.append({
297
  "role": "assistant", "message": f"❌ Error: {str(e)}", "time": datetime.now().strftime("%H:%M:%S")
298
  })
299
- yield {"type": "error", "session": session}
300
 
301
  # ================= Step 2: Search POIs =================
302
 
303
  def run_step2_search(self, session: UserSession) -> Dict[str, Any]:
304
  # 🌟 [Fix] 獲取 Live Session
305
  session = self._get_live_session(session)
 
 
 
306
 
307
  self._add_reasoning(session, "team", "🚀 Core Team activated")
308
  self._add_reasoning(session, "scout", "Searching for POIs...")
@@ -312,17 +415,24 @@ class PlannerService:
312
 
313
  def run_step3_team(self, session: UserSession) -> Generator[Dict[str, Any], None, None]:
314
  try:
315
- # 🌟 [Fix] 獲取 Live Session (關鍵:這裡才有 Agent)
316
  session = self._get_live_session(session)
317
 
 
 
 
 
 
 
 
 
 
 
 
318
  if session.planner_agent is None:
319
  raise ValueError("Agents not initialized. Please run Step 1 first.")
320
 
321
- set_session_id(session.session_id)
322
-
323
- # 從 Planner Agent 的 Memory 中獲取 Task List
324
  task_list_input = session.planner_agent.get_session_state().get("task_list")
325
-
326
  if isinstance(task_list_input, dict) or isinstance(task_list_input, list):
327
  task_list_str = json.dumps(task_list_input, indent=2, ensure_ascii=False)
328
  else:
@@ -332,6 +442,8 @@ class PlannerService:
332
  yield {"type": "start", "session": session}
333
 
334
  # 執行 Team Run
 
 
335
  team_stream = session.core_team.run(
336
  f"Plan this trip: {task_list_str}",
337
  stream=True, stream_events=True, session_id=session.session_id
@@ -363,7 +475,7 @@ class PlannerService:
363
 
364
  final_ref_id = poi_repo.get_last_id_by_session(session.session_id)
365
  if not final_ref_id:
366
- raise ValueError("No final result found")
367
 
368
  structured_data = poi_repo.load(final_ref_id)
369
 
 
54
 
55
  def _get_live_session(self, incoming_session: UserSession) -> UserSession:
56
  """
57
+ Gradio 傳來的 incoming_session 恢復 Live Session
 
 
58
  """
59
  sid = incoming_session.session_id
60
 
61
+ if not sid:
62
+ return incoming_session
63
+
64
+ # 1. 如果內存中有此 Session
65
  if sid and sid in self._active_sessions:
66
  live_session = self._active_sessions[sid]
67
+
68
+ # 同步基礎數據
69
  live_session.lat = incoming_session.lat
70
  live_session.lng = incoming_session.lng
71
+
72
+ # 🔥 關鍵修正:確保 API Settings 不會被覆蓋為空
73
+ # 如果 incoming 有設定,就用 incoming 的;否則保留 live 的
74
+ if incoming_session.custom_settings:
75
+ # 合併設定,優先使用 incoming (前端最新)
76
+ live_session.custom_settings.update(incoming_session.custom_settings)
77
+
78
+ # 同步聊天記錄
79
  if len(incoming_session.chat_history) > len(live_session.chat_history):
80
  live_session.chat_history = incoming_session.chat_history
81
+
82
  return live_session
83
 
84
+ # 2. 如果是新的 Session,註冊它
85
+ self._active_sessions[sid] = incoming_session
 
86
  return incoming_session
87
 
88
+ def _inject_tool_instance(self, tool_instance, session_id):
89
+ """
90
+ Monkey Patch: 在 Tool 執行前,將 Session ID 寫入 Repo 的 Thread Local。
91
+ 這樣 Repo.save() 就能在同一個執行緒中讀到 ID。
92
+ """
93
+
94
+ for attr_name in dir(tool_instance):
95
+ if attr_name.startswith("_"): continue
96
+ attr = getattr(tool_instance, attr_name)
97
+
98
+ if callable(attr):
99
+ if attr_name in ["register", "toolkit_id"]: continue
100
+
101
+ def create_wrapper(original_func, sid):
102
+ def wrapper(*args, **kwargs):
103
+ # 🔥 1. 設定 Thread Local Context
104
+ poi_repo.set_thread_session(sid)
105
+ try:
106
+ # 🔥 2. 執行 Tool
107
+ return original_func(*args, **kwargs)
108
+ finally:
109
+ # (可選) 清理 context,防止污染線程池中的下一個任務
110
+ # 但在单次請求中,保留著通常也沒問題,覆蓋即可
111
+ pass
112
+
113
+ return wrapper
114
+
115
+ setattr(tool_instance, attr_name, create_wrapper(attr, session_id))
116
+
117
+
118
+
119
  def initialize_agents(self, session: UserSession, lat: float, lng: float) -> UserSession:
120
+ if not session.session_id:
121
+ session.session_id = str(uuid.uuid4())
122
+ logger.info(f"🆔 Generated New Session ID: {session.session_id}")
123
 
124
+
125
+ session = self._get_live_session(session)
126
  session.lat = lat
127
  session.lng = lng
128
 
129
+ if not session.user_state:
130
+ session.user_state = UserState(location=Location(lat=lat, lng=lng))
131
+ else:
132
+ session.user_state.location = Location(lat=lat, lng=lng)
133
+
134
  if session.planner_agent is not None:
135
+ logger.info(f"♻️ Agents already initialized for {session.session_id}")
136
  return session
137
 
138
+ # 1. API Key 檢查
139
+ gemini_key = session.custom_settings.get("gemini_api_key")
140
+ google_key = session.custom_settings.get("google_maps_api_key")
141
+ weather_key = session.custom_settings.get("openweather_api_key")
 
 
 
 
 
 
142
 
143
+ import os
144
+ if not gemini_key: gemini_key = os.getenv("GEMINI_API_KEY", "")
145
+ if not google_key: google_key = os.getenv("GOOGLE_MAPS_API_KEY", "")
146
+ if not weather_key: weather_key = os.getenv("OPENWEATHER_API_KEY", "")
147
 
148
+ if not gemini_key:
149
+ raise ValueError("🔑 Missing Gemini API Key. Please configure Settings.")
 
 
 
150
 
151
+ # 2. 準備 Models
152
+ planner_model = Gemini(id='gemini-2.5-flash', thinking_budget=2048, api_key=gemini_key)
153
+ main_model = Gemini(id='gemini-2.5-flash', thinking_budget=1024, api_key=gemini_key)
154
+ lite_model = Gemini(id="gemini-2.5-flash-lite", api_key=gemini_key)
155
 
156
  models_dict = {
157
  "team": main_model, "scout": main_model, "optimizer": lite_model,
158
  "navigator": lite_model, "weatherman": lite_model, "presenter": main_model,
159
  }
160
 
161
+ # 3. 準備 Tools (先實例化,還不要給 Agent)
162
+ scout_tool = ScoutToolkit(google_key)
163
+ optimizer_tool = OptimizationToolkit(google_key)
164
+ navigator_tool = NavigationToolkit(google_key)
165
+ weather_tool = WeatherToolkit(weather_key)
166
+ reader_tool = ReaderToolkit()
167
+
168
+ # 4. 🔥 執行注入!確保所有 Agent 的 Tools 都帶有 Session ID
169
+ self._inject_tool_instance(scout_tool, session.session_id)
170
+ self._inject_tool_instance(optimizer_tool, session.session_id)
171
+ self._inject_tool_instance(navigator_tool, session.session_id)
172
+ self._inject_tool_instance(weather_tool, session.session_id)
173
+ self._inject_tool_instance(reader_tool, session.session_id)
174
+
175
+ # 5. 構建 Tools Dict (使用已注入的 Tool 實例)
176
  tools_dict = {
177
+ "scout": [scout_tool],
178
+ "optimizer": [optimizer_tool],
179
+ "navigator": [navigator_tool],
180
+ "weatherman": [weather_tool],
181
+ "presenter": [reader_tool],
182
  }
183
 
184
  planner_kwargs = {
185
+ "additional_context": get_context(session.user_state), # 這裡現在安全了
186
  "timezone_identifier": session.user_state.utc_offset,
187
  "debug_mode": False,
188
  }
189
 
190
  team_kwargs = {"timezone_identifier": session.user_state.utc_offset}
191
 
192
+ # 6. 建立 Agents
193
  session.planner_agent = create_planner_agent(planner_model, planner_kwargs, session_id=session.session_id)
194
  session.core_team = create_core_team(models_dict, team_kwargs, tools_dict, session_id=session.session_id)
195
 
196
+ self._active_sessions[session.session_id] = session
197
+
198
  logger.info(f"✅ Agents initialized for session {session.session_id}")
199
  return session
200
 
201
  # ================= Step 1: Analyze Tasks =================
202
 
203
  def run_step1_analysis(self, user_input: str, auto_location: bool,
204
+ lat: float, lon: float, session: UserSession) -> Generator[Dict[str, Any], None, None]:
205
+
206
+ # 🛡️ 檢查 1: 用戶輸入是否為空
207
+ if not user_input or len(user_input.strip()) == 0:
208
+ yield {
209
+ "type": "error",
210
+ "message": "⚠️ Please enter your plans first!",
211
+ "stream_text": "Waiting for input...",
212
+ "block_next_step": True # 🔥 新增 flag 阻止 UI 跳轉
213
+ }
214
+ return
215
+
216
+ # 🛡️ 驗證 2: 位置檢查
217
+ # 前端 JS 如果失敗會回傳 0, 0
218
+ if auto_location and (lat == 0 or lon == 0):
219
+ yield {
220
+ "type": "error",
221
+ "message": "⚠️ Location detection failed. Please uncheck 'Auto-detect' and enter manually.",
222
+ "stream_text": "Location Error...",
223
+ "block_next_step": True
224
+ }
225
  return
226
 
227
+ if not auto_location and (lat is None or lon is None):
228
+ yield {
229
+ "type": "error",
230
+ "message": "⚠️ Please enter valid Latitude/Longitude.",
231
+ "stream_text": "Location Error...",
232
+ "block_next_step": True
233
+ }
234
+ return
235
 
236
  try:
 
237
  session = self.initialize_agents(session, lat, lon)
238
 
239
  # 階段 1: 初始化
 
244
  "agent_status": ("planner", "working", "Initializing..."),
245
  "session": session
246
  }
 
247
 
248
+ # 階段 2: 提取任務 (即時串流優化)
249
  self._add_reasoning(session, "planner", f"Processing: {user_input[:50]}...")
250
  current_text = "🤔 Analyzing your request with AI...\n📋 AI is extracting tasks..."
 
 
 
 
 
 
251
 
252
+ # 呼叫 Agent
253
  planner_stream = session.planner_agent.run(
254
  f"help user to update the task_list, user's message: {user_input}",
255
  stream=True, stream_events=True
 
262
  if chunk.event == RunEvent.run_content:
263
  content = chunk.content
264
  accumulated_response += content
265
+ if "@@@" not in accumulated_response: # 簡單過濾 JSON 標記
266
  displayed_text += content
267
+ # 🔥 確保每個 Chunk 都觸發 UI 更新
268
  yield {
269
  "type": "stream",
270
  "stream_text": displayed_text,
271
+ "agent_status": ("planner", "working", "Thinking..."),
272
  "session": session
273
  }
274
 
 
288
  logger.error(f"Failed to parse task_list: {e}")
289
  session.task_list = []
290
 
291
+ # 🛡️ 檢查 2: Planner 是否回傳空列表
292
+ if not session.task_list or len(session.task_list) == 0:
293
+ err_msg = "⚠️ AI couldn't identify any tasks."
294
+ self._add_reasoning(session, "planner", " No tasks found")
295
+ yield {
296
+ "type": "error",
297
+ "message": err_msg,
298
+ "stream_text": err_msg,
299
+ "session": session,
300
+ "block_next_step": True # 🔥 阻止 UI 跳轉
301
+ }
302
+ return
303
+
304
+ # 成功
305
  yield {
306
  "type": "complete",
307
+ "stream_text": "Analysis complete!",
308
+ "high_priority": 0, # 計算邏輯...
309
+ "total_time": 0, # 計算邏輯...
310
+ "session": session,
311
+ "block_next_step": False # 允許跳轉
312
  }
313
 
314
  except Exception as e:
315
+ logger.error(f"Error: {e}")
316
+ yield {"type": "error", "message": str(e), "session": session, "block_next_step": True}
317
 
318
  # ================= Task Modification (Chat) =================
319
 
320
  def modify_task_chat(self, user_message: str, session: UserSession) -> Generator[Dict[str, Any], None, None]:
321
+
322
+ # 🛡️ 檢查 3: Chat 輸入是否為空
323
+ if not user_message or len(user_message.replace(' ', '')) == 0:
324
+ yield {
325
+ "type": "chat_error",
326
+ "message": "Please enter a message.",
327
+ "session": session
328
+ }
329
  return
330
 
 
331
  session = self._get_live_session(session)
332
 
333
+ # 用戶輸入上屏
334
  session.chat_history.append({
335
  "role": "user", "message": user_message, "time": datetime.now().strftime("%H:%M:%S")
336
  })
 
338
 
339
  try:
340
  if session.planner_agent is None:
 
341
  if session.lat and session.lng:
342
  session = self.initialize_agents(session, session.lat, session.lng)
343
  else:
344
+ yield {"type": "chat_error", "message": "Session lost. Please restart.", "session": session}
345
  return
346
 
347
+ # Agent 思考中
348
  session.chat_history.append({
349
  "role": "assistant", "message": "🤔 AI is thinking...", "time": datetime.now().strftime("%H:%M:%S")
350
  })
 
360
  if chunk.event == RunEvent.run_content:
361
  content = chunk.content
362
  accumulated_response += content
363
+ # 可選:在這裡也可以做 stream update 讓 chat bubble 動態出現文字
364
 
365
  json_data = "{" + accumulated_response.split("{", maxsplit=1)[-1]
366
  json_data = json_data.replace("`", "").replace("@", "").replace("\\", " ").replace("\n", " ")
 
373
  task_list_data = json.loads(json_data)
374
  session.task_list = self._convert_task_list_to_ui_format(task_list_data)
375
 
376
+ # 🔥 更新 Summary (回應 "Does summary still exist?" -> Yes!)
377
+ high_priority = sum(1 for t in session.task_list if t.get("priority") == "HIGH")
378
+ total_time = sum(int(t.get("duration", "0").split()[0]) for t in session.task_list if t.get("duration"))
379
+ summary_html = create_summary_card(len(session.task_list), high_priority, total_time)
380
+
381
  session.chat_history[-1] = {
382
  "role": "assistant",
383
  "message": "✅ Tasks updated based on your request",
 
385
  }
386
  self._add_reasoning(session, "planner", f"Updated: {user_message[:30]}...")
387
 
388
+ yield {
389
+ "type": "complete",
390
+ "summary_html": summary_html, # 回傳新的 Summary
391
+ "session": session
392
+ }
393
 
394
  except Exception as e:
395
  logger.error(f"Chat error: {e}")
396
  session.chat_history.append({
397
  "role": "assistant", "message": f"❌ Error: {str(e)}", "time": datetime.now().strftime("%H:%M:%S")
398
  })
399
+ yield {"type": "update_history", "session": session}
400
 
401
  # ================= Step 2: Search POIs =================
402
 
403
  def run_step2_search(self, session: UserSession) -> Dict[str, Any]:
404
  # 🌟 [Fix] 獲取 Live Session
405
  session = self._get_live_session(session)
406
+ if session.session_id:
407
+ set_session_id(session.session_id)
408
+ logger.info(f"🔄 [Step 2] Session Context Set: {session.session_id}")
409
 
410
  self._add_reasoning(session, "team", "🚀 Core Team activated")
411
  self._add_reasoning(session, "scout", "Searching for POIs...")
 
415
 
416
  def run_step3_team(self, session: UserSession) -> Generator[Dict[str, Any], None, None]:
417
  try:
 
418
  session = self._get_live_session(session)
419
 
420
+ # 🔥 修正 2: 確保 Session ID 在這個執行緒中生效
421
+ if session.session_id:
422
+ set_session_id(session.session_id)
423
+ logger.info(f"🔄 [Step 3] Session Context Set: {session.session_id}")
424
+ else:
425
+ logger.warning("⚠️ [Step 3] No Session ID found!")
426
+
427
+ if not session.task_list:
428
+ yield {"type": "error", "message": "No tasks to plan.", "session": session}
429
+ return
430
+
431
  if session.planner_agent is None:
432
  raise ValueError("Agents not initialized. Please run Step 1 first.")
433
 
434
+ # 準備輸入
 
 
435
  task_list_input = session.planner_agent.get_session_state().get("task_list")
 
436
  if isinstance(task_list_input, dict) or isinstance(task_list_input, list):
437
  task_list_str = json.dumps(task_list_input, indent=2, ensure_ascii=False)
438
  else:
 
442
  yield {"type": "start", "session": session}
443
 
444
  # 執行 Team Run
445
+ # 注意:如果 Agno 內部使用 ThreadPool,Context 還是可能遺失。
446
+ # 但既然之前能跑,代表 Agno 應該是在當前執行緒或兼容的 Context 下運行工具的。
447
  team_stream = session.core_team.run(
448
  f"Plan this trip: {task_list_str}",
449
  stream=True, stream_events=True, session_id=session.session_id
 
475
 
476
  final_ref_id = poi_repo.get_last_id_by_session(session.session_id)
477
  if not final_ref_id:
478
+ raise ValueError(f"No final result found , {session.session_id}")
479
 
480
  structured_data = poi_repo.load(final_ref_id)
481
 
src/infra/poi_repository.py CHANGED
@@ -3,7 +3,10 @@ import json
3
  import uuid
4
  import os
5
  from typing import Any, Dict, Optional
6
- from src.infra.context import get_session_id # ✅ 引入 Context
 
 
 
7
 
8
 
9
  class PoiRepository:
@@ -42,9 +45,9 @@ class PoiRepository:
42
  current_session = get_session_id()
43
  if current_session:
44
  self._session_last_id[current_session] = ref_id
45
- print(f"💾 [Repo] Saved {ref_id} for Session: {current_session}")
46
  else:
47
- print(f"⚠️ [Repo] Warning: No session context found! 'last_id' not tracked.")
48
 
49
  return ref_id
50
 
 
3
  import uuid
4
  import os
5
  from typing import Any, Dict, Optional
6
+ from src.infra.context import get_session_id
7
+ from src.infra.logger import get_logger
8
+
9
+ logger = get_logger(__name__)
10
 
11
 
12
  class PoiRepository:
 
45
  current_session = get_session_id()
46
  if current_session:
47
  self._session_last_id[current_session] = ref_id
48
+ logger.info(f"💾 [Repo] Saved {ref_id} for Session: {current_session}")
49
  else:
50
+ logger.warning(f"⚠️ [Repo] Warning: No session context found! 'last_id' not tracked.")
51
 
52
  return ref_id
53