Marco310 commited on
Commit
50a7f50
·
1 Parent(s): a7c1db4

Refactor Step 3: Fix threading context loss, enable real-time streaming, and optimize UI/UX flow.

Browse files

- Core Fix: Implemented `patch_repo_context` (monkey patch) in `planner_service` to resolve session ID loss during multi-threaded tool execution.
- Service: Refactored `run_step3_team` to yield real-time events (reasoning, report stream, agent status) to the UI.
- UI/App: Updated `step3_wrapper` and `build_interface` wiring to ensure all 9 outputs are correctly mapped, fixing the frozen UI issue.
- UX: Enabled immediate visibility of Report/Map tabs upon planning start and added auto-switch logic to the Report tab.
- Cleanup: Synced agent lists (removed 'planner', confirmed 'presenter') across `app.py` and `results.py`."

app.py CHANGED
@@ -15,6 +15,7 @@ from config import APP_TITLE, DEFAULT_SETTINGS
15
  from ui.theme import get_enhanced_css
16
  from ui.renderers import (
17
  create_agent_stream_output,
 
18
  create_agent_card_enhanced,
19
  get_reasoning_html_reversed,
20
  generate_chat_history_html_bubble
@@ -48,15 +49,39 @@ class LifeFlowAI:
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
  # -------------------------------------------------------------------------
@@ -65,7 +90,7 @@ class LifeFlowAI:
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(),
@@ -80,78 +105,90 @@ class LifeFlowAI:
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
  )
@@ -164,10 +201,12 @@ class LifeFlowAI:
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,
@@ -179,17 +218,38 @@ class LifeFlowAI:
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)
@@ -198,7 +258,7 @@ class LifeFlowAI:
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",
@@ -310,7 +370,12 @@ class LifeFlowAI:
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
  # ---------------------------------------------------------------------
@@ -321,15 +386,28 @@ class LifeFlowAI:
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(
 
15
  from ui.theme import get_enhanced_css
16
  from ui.renderers import (
17
  create_agent_stream_output,
18
+ create_summary_card,
19
  create_agent_card_enhanced,
20
  get_reasoning_html_reversed,
21
  generate_chat_history_html_bubble
 
49
 
50
  def _get_agent_outputs(self, active_agent: str = None, status: str = "idle", message: str = "Waiting") -> List[str]:
51
  """輔助函數:生成 Agent 卡片 HTML"""
52
+
53
+ # 🔥 [關鍵修正] 更新為你的新 Agent 名單 (共 5 個)
54
+ # 必須與 create_team_area 中的順序完全一致!
55
+ agents = ['team', 'scout', 'optimizer', 'navigator', 'weatherman', 'presenter']
56
+
57
  outputs = []
58
  for agent in agents:
59
  if agent == active_agent:
60
  outputs.append(create_agent_card_enhanced(agent, status, message))
61
  else:
62
  outputs.append(create_agent_card_enhanced(agent, "idle", "On standby"))
63
+
64
  return outputs
65
 
66
+ def _update_task_summary(self, session: UserSession) -> str:
67
+ """統一計算並回傳 Summary Card HTML"""
68
+ tasks = session.task_list
69
+ if not tasks:
70
+ return create_summary_card(0, 0, 0)
71
+
72
+ high_priority = sum(1 for t in tasks if t.get("priority", "").upper() == "HIGH")
73
+
74
+ total_time = 0
75
+ for t in tasks:
76
+ dur_str = str(t.get("duration", "0"))
77
+ try:
78
+ val = int(dur_str.split()[0])
79
+ total_time += val
80
+ except (ValueError, IndexError):
81
+ pass
82
+
83
+ return create_summary_card(len(tasks), high_priority, total_time)
84
+
85
  # -------------------------------------------------------------------------
86
  # Step 1: Analyze (分析任務)
87
  # -------------------------------------------------------------------------
 
90
 
91
  # 1. 輸入驗證
92
  if not user_input or not user_input.strip():
93
+ agent_outputs = self._get_agent_outputs("team", "idle", "Waiting")
94
  yield (
95
  "<div style='color: #ef4444; font-weight: bold; padding: 5px;'>⚠️ Please describe your plans first!</div>",
96
  gr.HTML(), gr.HTML(),
 
105
 
106
  for event in iterator:
107
  evt_type = event.get("type")
108
+ agent_status = event.get("agent_status", ("team", "idle", "Waiting"))
109
  agent_outputs = self._get_agent_outputs(*agent_status)
110
  current_session = event.get("session", session)
111
  reasoning_html = get_reasoning_html_reversed(current_session.reasoning_messages)
112
 
113
  if evt_type == "stream":
114
+ html_output = create_agent_stream_output(event.get("stream_text", ""))
115
  yield (
116
+ html_output,
117
  gr.HTML(), gr.HTML(), reasoning_html,
118
  gr.update(visible=False), gr.update(visible=False), gr.HTML(),
119
  f"Processing: {agent_status[2]}", gr.update(visible=True), *agent_outputs, current_session.to_dict()
120
  )
121
  elif evt_type == "complete":
122
+ summary_html = self._update_task_summary(current_session)
123
+
124
  task_html = self.service.generate_task_list_html(current_session)
125
+
126
+ final_text = event.get("stream_text", "Analysis complete!")
127
+ html_output = create_agent_stream_output(final_text)
128
+
129
+ final_agents = self._get_agent_outputs("team", "complete", "Tasks ready")
130
+
131
  yield (
132
+ html_output,
133
+ gr.HTML(value=summary_html), # ✅ 更新 Summary Component
134
+ gr.HTML(value=task_html),
135
+ reasoning_html,
136
  gr.update(visible=True), gr.update(visible=True),
137
  generate_chat_history_html_bubble(current_session),
138
  "✓ Tasks extracted", gr.update(visible=False), *final_agents, current_session.to_dict()
139
  )
140
+
141
  elif evt_type == "error":
142
  err_msg = event.get("message", "Unknown error")
143
+ error_agents = self._get_agent_outputs("team", "idle", "Error")
144
+
145
  yield (
146
  f"<div style='color:red'>Error: {err_msg}</div>", gr.HTML(), gr.HTML(), reasoning_html,
147
  gr.update(visible=False), gr.update(visible=False), gr.HTML(),
148
  f"Error: {err_msg}", gr.update(visible=True), *error_agents, current_session.to_dict()
149
  )
150
 
151
+
152
  # -------------------------------------------------------------------------
153
  # Chat Modification (修改任務)
154
  # -------------------------------------------------------------------------
155
  def chat_wrapper(self, msg, session_data):
156
  session = UserSession.from_dict(session_data)
157
  iterator = self.service.modify_task_chat(msg, session)
158
+
159
  for event in iterator:
160
  current_session = event.get("session", session)
161
+
162
+ # 🔥 [Update 2] Step 2 Modify: 每次對話更新後,重新計算 Summary
163
+ new_summary_html = self._update_task_summary(current_session)
164
+
165
  yield (
166
+ generate_chat_history_html_bubble(current_session), # Output 1: Chat History
167
+ self.service.generate_task_list_html(current_session), # Output 2: Task List
168
+ new_summary_html, # Output 3: ✅ Summary Card (新增這個)
169
+ current_session.to_dict() # Output 4: Session State
170
  )
171
 
172
  # -------------------------------------------------------------------------
173
  # Step 2 -> 3 Transition (驗證與過渡)
174
  # -------------------------------------------------------------------------
175
  def transition_to_planning(self, session_data):
 
 
 
 
 
 
176
  session = UserSession.from_dict(session_data)
177
 
178
  # 1. 🛑 防呆檢查
179
  if not session.task_list or len(session.task_list) == 0:
180
+ agent_outputs = self._get_agent_outputs("team", "idle", "No tasks")
181
  reasoning = get_reasoning_html_reversed(session.reasoning_messages)
182
 
183
  return (
184
+ gr.update(visible=True), # task_confirm_area
185
  gr.update(visible=True), # chat_input_area
186
+ gr.update(visible=False), # team_area
187
+ gr.update(), # tabs
188
+ gr.update(), # 🔥 [新增] report_tab (維持現狀)
189
+ gr.update(), # 🔥 [新增] map_tab (維持現狀)
190
  reasoning,
191
+ "⚠️ Error: No tasks to plan! Please add tasks.",
192
  *agent_outputs,
193
  session.to_dict()
194
  )
 
201
  agent_outputs = self._get_agent_outputs("scout", "working", "Searching POIs...")
202
 
203
  return (
204
+ gr.update(visible=False), # task_confirm_area
205
+ gr.update(visible=False), # chat_input_area
206
+ gr.update(visible=True), # team_area
207
+ gr.update(selected="report_tab"), # tabs (預設看 Log)
208
+ gr.update(visible=True), # 🔥 [關鍵修改] report_tab 設為可見!
209
+ gr.update(visible=False), # 🔥 [關鍵修改] map_tab 設為可見!
210
  reasoning_html,
211
  "🗺️ Scout is searching locations...",
212
  *agent_outputs,
 
218
  # -------------------------------------------------------------------------
219
  def step3_wrapper(self, session_data):
220
  session = UserSession.from_dict(session_data)
221
+
222
+ # 1. 防呆回傳 (9個值)
223
+ if not session.task_list:
224
+ agent_outputs = self._get_agent_outputs("team", "idle", "No tasks")
225
+ # yield 順序: Report, Reasoning, Agents(6個), Session
226
+ yield ("", "", *agent_outputs, session.to_dict())
227
  return
228
 
229
  iterator = self.service.run_step3_team(session)
230
+ current_report = "⏳ Generating plan..."
231
+
232
  for event in iterator:
233
+ evt_type = event.get("type")
234
  current_session = event.get("session", session)
235
+
236
+ # 準備 UI Data
237
+ reasoning_html = get_reasoning_html_reversed(current_session.reasoning_messages)
238
+ agent_status = event.get("agent_status", ("team", "working", "Processing..."))
239
+ agent_outputs = self._get_agent_outputs(*agent_status)
240
+
241
+ # 更新 Report 暫存
242
+ if evt_type == "report_stream":
243
+ current_report = event.get("content", current_report)
244
+
245
+ # 🔥 統一回傳 (確保任何事件都更新所有 UI)
246
+ # 必須是 9 個值:Report, Reasoning, Agent1...6, Session
247
+ yield (
248
+ current_report,
249
+ reasoning_html,
250
+ *agent_outputs,
251
+ current_session.to_dict()
252
+ )
253
 
254
  # -------------------------------------------------------------------------
255
  # Step 4 Wrapper (Optimization & Results)
 
258
  session = UserSession.from_dict(session_data)
259
  if not session.task_list: # 二次防呆
260
  default_map = create_animated_map()
261
+ agent_outputs = self._get_agent_outputs("team", "idle", "No tasks")
262
  return (
263
  "", "", "", default_map,
264
  gr.update(), gr.update(), "⚠️ Planning aborted",
 
370
  chat_event = chat_send.click(
371
  fn=self.chat_wrapper,
372
  inputs=[chat_input, session_state],
373
+ outputs=[
374
+ chat_history_output, # 1. 對話框
375
+ task_list_display, # 2. 任務列表
376
+ task_summary_display, # 🔥 [Critical Fix] 3. 摘要卡片 (必須加這行!)
377
+ session_state # 4. Session
378
+ ]
379
  ).then(fn=lambda: "", outputs=[chat_input])
380
 
381
  # ---------------------------------------------------------------------
 
386
  fn=self.transition_to_planning,
387
  inputs=[session_state],
388
  outputs=[
389
+ task_confirm_area,
390
+ chat_input_area,
391
+ team_area,
392
+ tabs,
393
+ report_tab, # 🔥 [新增] 這裡要接 report_tab
394
+ map_tab, # 🔥 [新增] 這裡要接 map_tab
395
+ reasoning_output,
396
+ status_bar,
397
+ *agent_displays,
398
+ session_state
399
  ]
400
  )
401
 
402
  step3_event = step2_event.then(
403
  fn=self.step3_wrapper,
404
  inputs=[session_state],
405
+ outputs=[
406
+ report_output, # 1. 報告 Tab
407
+ reasoning_output, # 2. AI Conversation Log (這裡漏掉了!)
408
+ *agent_displays, # 3-8. Agent Grid (這裡也漏掉了!)
409
+ session_state # 9. Session
410
+ ]
411
  )
412
 
413
  step3_ui_update = step3_event.then(
services/planner_service.py CHANGED
@@ -7,6 +7,7 @@ import time
7
  import uuid
8
  from datetime import datetime
9
  from typing import Generator, Dict, Any, Tuple, Optional
 
10
 
11
  # 導入 Core & UI 模組
12
  from core.session import UserSession
@@ -43,6 +44,44 @@ from src.infra.logger import get_logger
43
 
44
  logger = get_logger(__name__)
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  class PlannerService:
47
  """
48
  PlannerService 封裝了所有的業務流程 (Step 1-4)。
@@ -264,10 +303,11 @@ class PlannerService:
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
  }
@@ -286,6 +326,7 @@ class PlannerService:
286
  session.task_list = self._convert_task_list_to_ui_format(task_list_data)
287
  except Exception as e:
288
  logger.error(f"Failed to parse task_list: {e}")
 
289
  session.task_list = []
290
 
291
  # 🛡️ 檢查 2: Planner 是否回傳空列表
@@ -360,7 +401,11 @@ class PlannerService:
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", " ")
@@ -407,8 +452,6 @@ class PlannerService:
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...")
412
  return {"session": session}
413
 
414
  # ================= Step 3: Run Core Team =================
@@ -417,51 +460,107 @@ class PlannerService:
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:
439
- task_list_str = str(task_list_input)
440
 
441
  self._add_reasoning(session, "team", "🎯 Multi-agent collaboration started")
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
450
- )
451
 
452
- report_content = ""
453
- for event in team_stream:
454
- if event.event in [TeamRunEvent.run_content]:
455
- report_content += event.content
456
- elif event.event == "tool_call":
457
- tool_name = event.tool_call.get('function', {}).get('name', 'unknown')
458
- self._add_reasoning(session, "team", f"🔧 Tool: {tool_name}")
459
- yield {"type": "reasoning_update", "session": session}
460
- elif event.event == TeamRunEvent.run_completed:
461
- self._add_reasoning(session, "team", "🎉 Completed")
 
 
 
 
 
 
 
 
 
 
462
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  report_html = f"## 🎯 Planning Complete\n\n{report_content}"
464
- yield {"type": "complete", "report_html": report_html, "session": session}
 
 
 
 
 
465
 
466
  except Exception as e:
467
  logger.error(f"Team run error: {e}", exc_info=True)
 
7
  import uuid
8
  from datetime import datetime
9
  from typing import Generator, Dict, Any, Tuple, Optional
10
+ from contextlib import contextmanager
11
 
12
  # 導入 Core & UI 模組
13
  from core.session import UserSession
 
44
 
45
  logger = get_logger(__name__)
46
 
47
+
48
+ @contextmanager
49
+ def patch_repo_context(session_id: str):
50
+ """
51
+ 暴力解決方案:
52
+ 在執行期間,暫時將 poi_repo 的 save/add 方法「綁架」,
53
+ 強迫它們在執行前,先設定當前的 Thread Session。
54
+ """
55
+
56
+ # 1. 備份原始方法
57
+ original_save = getattr(poi_repo, 'save', None)
58
+ original_add = getattr(poi_repo, 'add', None)
59
+
60
+ # 2. 定義「綁架」後的方法
61
+ def patched_save(*args, **kwargs):
62
+ set_session_id(session_id)
63
+ # 呼叫原始方法
64
+ if original_save:
65
+ return original_save(*args, **kwargs)
66
+ elif original_add:
67
+ return original_add(*args, **kwargs)
68
+
69
+ # 3. 執行替換
70
+ if original_save:
71
+ setattr(poi_repo, 'save', patched_save)
72
+ if original_add:
73
+ setattr(poi_repo, 'add', patched_save)
74
+
75
+ try:
76
+ yield
77
+ finally:
78
+ # 4. 恢復原始方法
79
+ if original_save:
80
+ setattr(poi_repo, 'save', original_save)
81
+ if original_add:
82
+ setattr(poi_repo, 'add', original_add)
83
+
84
+
85
  class PlannerService:
86
  """
87
  PlannerService 封裝了所有的業務流程 (Step 1-4)。
 
303
  accumulated_response += content
304
  if "@@@" not in accumulated_response: # 簡單過濾 JSON 標記
305
  displayed_text += content
306
+
307
+ formatted_text = displayed_text.replace("\n", "<br/>")
308
  yield {
309
  "type": "stream",
310
+ "stream_text": formatted_text,
311
  "agent_status": ("planner", "working", "Thinking..."),
312
  "session": session
313
  }
 
326
  session.task_list = self._convert_task_list_to_ui_format(task_list_data)
327
  except Exception as e:
328
  logger.error(f"Failed to parse task_list: {e}")
329
+ print(json_data)
330
  session.task_list = []
331
 
332
  # 🛡️ 檢查 2: Planner 是否回傳空列表
 
401
  if chunk.event == RunEvent.run_content:
402
  content = chunk.content
403
  accumulated_response += content
404
+ if "@@@" not in accumulated_response: # 簡單過濾 JSON 標記
405
+ accumulated_response += content
406
+ formatted_text = accumulated_response.replace("\n", "<br/>")
407
+ session.chat_history[-1]["message"] += formatted_text
408
+ yield {"type": "update_history", "session": session}
409
 
410
  json_data = "{" + accumulated_response.split("{", maxsplit=1)[-1]
411
  json_data = json_data.replace("`", "").replace("@", "").replace("\\", " ").replace("\n", " ")
 
452
  set_session_id(session.session_id)
453
  logger.info(f"🔄 [Step 2] Session Context Set: {session.session_id}")
454
 
 
 
455
  return {"session": session}
456
 
457
  # ================= Step 3: Run Core Team =================
 
460
  try:
461
  session = self._get_live_session(session)
462
 
 
463
  if session.session_id:
464
  set_session_id(session.session_id)
465
  logger.info(f"🔄 [Step 3] Session Context Set: {session.session_id}")
 
 
466
 
467
  if not session.task_list:
468
  yield {"type": "error", "message": "No tasks to plan.", "session": session}
469
  return
470
 
471
+ # 準備 Task List String
 
 
 
472
  task_list_input = session.planner_agent.get_session_state().get("task_list")
473
+ task_list_str = json.dumps(task_list_input, indent=2, ensure_ascii=False) if isinstance(task_list_input, (
474
+ dict, list)) else str(task_list_input)
 
 
475
 
476
  self._add_reasoning(session, "team", "🎯 Multi-agent collaboration started")
 
 
 
 
 
 
 
 
 
477
 
478
+ # 🔥 初始狀態
479
+ yield {
480
+ "type": "reasoning_update",
481
+ "session": session,
482
+ "agent_status": ("team", "working", "Analyzing tasks...")
483
+ }
484
+
485
+ # 🔥 [CRITICAL FIX] 使用 Patch Context 包裹執行區塊
486
+ with patch_repo_context(session.session_id):
487
+
488
+ team_stream = session.core_team.run(
489
+ f"Plan this trip: {task_list_str}",
490
+ stream=True, stream_events=True, session_id=session.session_id
491
+ )
492
+
493
+
494
+ start_time = time.perf_counter()
495
+ report_content = ""
496
+ # 🔥 Event Loop: 捕捉事件並 yield 給 UI
497
+ for event in team_stream:
498
 
499
+ # 1. 報告內容 stream
500
+ if event.event == RunEvent.run_content and event.agent_id == "presenter":
501
+ report_content += event.content
502
+ yield {
503
+ "type": "report_stream",
504
+ "content": report_content,
505
+ "session": session
506
+ }
507
+
508
+ # 2. Team Delegate (分派任務)
509
+ if event.event == TeamRunEvent.tool_call_started:
510
+ tool_name = event.tool.tool_name
511
+ if "delegate_task_to_member" in tool_name:
512
+ member_id = event.tool.tool_args.get("member_id", "unknown")
513
+ msg = f"Delegating to {member_id}..."
514
+ self._add_reasoning(session, "team", f"👉 {msg}")
515
+
516
+ yield {
517
+ "type": "reasoning_update",
518
+ "session": session,
519
+ "agent_status": ("team", "working", msg)
520
+ }
521
+ else:
522
+ self._add_reasoning(session, "team", f"🔧 Tool: {tool_name}")
523
+
524
+ # 3. Member Tool Start (成員開始工作)
525
+ elif event.event == RunEvent.tool_call_started:
526
+ member_id = event.agent_id
527
+ tool_name = event.tool.tool_name
528
+ self._add_reasoning(session, member_id, f"Using tool: {tool_name}...")
529
+
530
+ yield {
531
+ "type": "reasoning_update",
532
+ "session": session,
533
+ "agent_status": (member_id, "working", f"Running {tool_name}...")
534
+ }
535
+
536
+ # 4. Member Tool End (成員工作結束)
537
+ elif event.event == RunEvent.tool_call_completed:
538
+ member_id = event.agent_id
539
+ self._add_reasoning(session, member_id, "✅ Task completed")
540
+
541
+ yield {
542
+ "type": "reasoning_update",
543
+ "session": session,
544
+ "agent_status": (member_id, "idle", "Done")
545
+ }
546
+
547
+ # 5. Team Complete
548
+ elif event.event == TeamRunEvent.run_completed:
549
+ self._add_reasoning(session, "team", "🎉 Planning process finished")
550
+
551
+ logger.info(f"Total tokens: {event.metrics.total_tokens}")
552
+ logger.info(f"Input tokens: {event.metrics.input_tokens}")
553
+ logger.info(f"Output tokens: {event.metrics.output_tokens}")
554
+ logger.info(f"Run time (s): {time.perf_counter() - start_time}")
555
+
556
+ # 迴圈結束
557
  report_html = f"## 🎯 Planning Complete\n\n{report_content}"
558
+ yield {
559
+ "type": "complete",
560
+ "report_html": report_html,
561
+ "session": session,
562
+ "agent_status": ("team", "complete", "Finished")
563
+ }
564
 
565
  except Exception as e:
566
  logger.error(f"Team run error: {e}", exc_info=True)
src/agent/setting/team.py CHANGED
@@ -49,7 +49,7 @@ instructions="""
49
  - 🛑 **CRITICAL**: You MUST use the ID starting with `final_itinerary_`. DO NOT reuse `navigation_result_`.
50
 
51
  #### STEP 6: FINISH
52
- - **Action**: Output the text returned by `Presenter` verbatim.
53
 
54
  ### 🛡️ EXCEPTION HANDLING
55
  - **Valid ID Patterns**: `scout_result_`, `optimization_result_`, `navigation_result_`, `final_itinerary_`.
 
49
  - 🛑 **CRITICAL**: You MUST use the ID starting with `final_itinerary_`. DO NOT reuse `navigation_result_`.
50
 
51
  #### STEP 6: FINISH
52
+ - **Action**: Only Return `Completed Task`.
53
 
54
  ### 🛡️ EXCEPTION HANDLING
55
  - **Valid ID Patterns**: `scout_result_`, `optimization_result_`, `navigation_result_`, `final_itinerary_`.
ui/components/results.py CHANGED
@@ -9,29 +9,28 @@ import gradio as gr
9
 
10
 
11
  def create_team_area(create_agent_card_func):
12
- """創建 AI Team 展示區域 (改為 Grid)"""
13
  with gr.Group(visible=False) as team_area:
14
  gr.Markdown("### 🤖 Agent Status")
15
- # 使用自定義 CSS grid wrapper
16
- with gr.Column():
17
- # 這裡我們需要一個容器來放入所有的 HTML 卡片,或者將它們合併為一個 HTML 輸出
18
- # 為了簡單起見,我們將 agent_displays 保持為 list,但在前端用 flex/grid 呈現
19
- # 注意:為了達到最佳 grid 效果,建議在 app.py 中將 agent updates 合併為一個 HTML string
20
- # 但為了不改動邏輯,我們這裡運用 CSS (.agent-grid) 包裹
21
-
22
- agent_container_start = gr.HTML('<div class="agent-grid">')
23
- agent_displays = []
24
- # 這裡對應 app.py 的邏輯,會生成多個 HTML 組件
25
- for agent_key in ['planner', 'scout', 'optimizer', 'validator', 'weather', 'traffic']:
26
- # 初始狀態
27
- card_html = create_agent_card_func(agent_key, "idle", "Waiting")
28
- agent_displays.append(gr.HTML(value=card_html, show_label=False))
29
-
30
- agent_container_end = gr.HTML('</div>')
31
-
32
-
33
- for display in agent_displays:
34
- display.elem_classes = ["agent-grid-item"]
35
 
36
  return team_area, agent_displays
37
 
 
9
 
10
 
11
  def create_team_area(create_agent_card_func):
12
+ """創建 AI Team 展示區域"""
13
  with gr.Group(visible=False) as team_area:
14
  gr.Markdown("### 🤖 Agent Status")
15
+
16
+ # 使用 HTML 容器
17
+ agent_container_start = gr.HTML('<div class="agent-grid">')
18
+ agent_displays = []
19
+
20
+ # 🔥 [關鍵修正] 這裡的名單必須跟 app.py 的 _get_agent_outputs 一模一樣!
21
+ target_agents = ['team', 'scout', 'optimizer', 'navigator', 'weatherman', 'presenter']
22
+
23
+ for agent_key in target_agents:
24
+ # 初始狀態
25
+ card_html = create_agent_card_func(agent_key, "idle", "Waiting")
26
+ # 存入 list,這會成為 build_interface 中的 outputs 元件
27
+ agent_displays.append(gr.HTML(value=card_html, show_label=False))
28
+
29
+ agent_container_end = gr.HTML('</div>')
30
+
31
+ # 套用 CSS Class
32
+ for display in agent_displays:
33
+ display.elem_classes = ["agent-grid-item"]
 
34
 
35
  return team_area, agent_displays
36
 
ui/renderers.py CHANGED
@@ -5,12 +5,40 @@ LifeFlow AI - UI Renderers
5
  from datetime import datetime
6
  from config import AGENTS_INFO
7
 
8
- def create_agent_stream_output() -> str:
9
- """創建 Agent 串流輸出 HTML"""
10
- return """
 
 
 
 
 
 
 
 
 
 
 
11
  <div class="stream-container">
12
- <div class="stream-text">Ready to analyze your tasks...<span class="stream-cursor"></span></div>
13
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  """
15
 
16
 
 
5
  from datetime import datetime
6
  from config import AGENTS_INFO
7
 
8
+
9
+ # ui/renderers.py
10
+
11
+ def create_agent_stream_output(text: str = None) -> str:
12
+ """創建 Agent 串流輸出 HTML (支援動態傳入文字)"""
13
+
14
+ # 如果沒有傳入文字,顯示預設值
15
+ if not text:
16
+ display_content = 'Ready to analyze your tasks...<span class="stream-cursor"></span>'
17
+ else:
18
+ # 這裡直接使用傳入的 text
19
+ display_content = f'{text}<span class="stream-cursor"></span>'
20
+
21
+ return f"""
22
  <div class="stream-container">
23
+ <div class="stream-text" style="white-space: pre-wrap; line-height: 1.6; min-height: 60px;">{display_content}</div>
24
  </div>
25
+ <style>
26
+ .stream-text {{
27
+ font-family: 'Inter', sans-serif;
28
+ color: #334155;
29
+ font-size: 1.05rem;
30
+ }}
31
+ .stream-cursor {{
32
+ display: inline-block;
33
+ width: 8px;
34
+ height: 18px;
35
+ background-color: #6366f1;
36
+ margin-left: 5px;
37
+ animation: blink 1s infinite;
38
+ vertical-align: sub;
39
+ }}
40
+ @keyframes blink {{ 0%, 100% {{ opacity: 1; }} 50% {{ opacity: 0; }} }}
41
+ </style>
42
  """
43
 
44