Spaces:
Running
Running
Feat: implement input validation and safe transition logic. Added task list checks before planning and fixed concurrency issues on app reset.
Browse files- app.py +156 -167
- services/planner_service.py +194 -82
- 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
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(),
|
| 91 |
-
gr.
|
| 92 |
-
|
| 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 |
-
#
|
| 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.
|
| 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 |
-
|
| 147 |
-
|
| 148 |
current_session.to_dict()
|
| 149 |
)
|
| 150 |
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 191 |
-
|
| 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 |
-
|
| 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),
|
| 240 |
-
gr.update(visible=False), gr.update(visible=False),
|
| 241 |
-
gr.update(visible=False), gr.update(visible=False),
|
| 242 |
-
gr.update(visible=False), "",
|
| 243 |
create_agent_stream_output(),
|
| 244 |
status_msg,
|
| 245 |
-
|
|
|
|
| 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 |
-
#
|
| 302 |
-
|
|
|
|
|
|
|
| 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,
|
|
|
|
| 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 |
-
#
|
| 316 |
-
|
|
|
|
|
|
|
| 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 |
-
#
|
| 323 |
-
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
| 325 |
inputs=[session_state],
|
| 326 |
outputs=[
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
agent_stream_output, status_bar, session_state # 確保 status_bar 在列表裡
|
| 330 |
]
|
| 331 |
)
|
| 332 |
|
| 333 |
-
|
| 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 |
-
)
|
|
|
|
|
|
|
| 360 |
fn=lambda: (gr.update(visible=True), gr.update(visible=True), gr.update(selected="report_tab")),
|
| 361 |
outputs=[report_tab, map_tab, tabs]
|
| 362 |
-
)
|
|
|
|
|
|
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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]
|
| 379 |
)
|
| 380 |
-
|
| 381 |
-
|
| 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 |
-
|
| 58 |
-
從 Gradio 傳來的 incoming_session 只有數據 (Agent 為 None)。
|
| 59 |
-
我們需要從 _active_sessions 取回包含 Agent 的真實 Session。
|
| 60 |
"""
|
| 61 |
sid = incoming_session.session_id
|
| 62 |
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
| 64 |
if sid and sid in self._active_sessions:
|
| 65 |
live_session = self._active_sessions[sid]
|
| 66 |
-
|
|
|
|
| 67 |
live_session.lat = incoming_session.lat
|
| 68 |
live_session.lng = incoming_session.lng
|
| 69 |
-
|
| 70 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
|
|
|
|
|
|
|
| 85 |
session.lat = lat
|
| 86 |
session.lng = lng
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
if session.planner_agent is not None:
|
|
|
|
| 89 |
return session
|
| 90 |
|
| 91 |
-
#
|
| 92 |
-
|
| 93 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 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=
|
| 113 |
-
main_model = Gemini(id=
|
| 114 |
-
lite_model = Gemini(id="gemini-2.5-flash-lite", 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": [
|
| 123 |
-
"optimizer": [
|
| 124 |
-
"navigator": [
|
| 125 |
-
"weatherman": [
|
| 126 |
-
"presenter": [
|
| 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 |
-
#
|
| 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 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
return
|
| 151 |
|
| 152 |
-
if auto_location:
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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", "
|
| 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 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
yield {
|
| 223 |
"type": "complete",
|
| 224 |
-
"stream_text":
|
| 225 |
-
"high_priority":
|
| 226 |
-
"total_time":
|
| 227 |
-
"session": session
|
|
|
|
| 228 |
}
|
| 229 |
|
| 230 |
except Exception as e:
|
| 231 |
-
logger.error(f"Error
|
| 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 |
-
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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": "
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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": "
|
| 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 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 46 |
else:
|
| 47 |
-
|
| 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 |
|