Spaces:
Running
Running
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 +118 -40
- services/planner_service.py +135 -36
- src/agent/setting/team.py +1 -1
- ui/components/results.py +20 -21
- ui/renderers.py +32 -4
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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("
|
| 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", ("
|
| 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 |
-
|
| 90 |
yield (
|
| 91 |
-
|
| 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 |
-
|
|
|
|
| 98 |
task_html = self.service.generate_task_list_html(current_session)
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
yield (
|
| 102 |
-
|
| 103 |
-
gr.HTML(value=summary_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("
|
|
|
|
| 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 |
-
|
|
|
|
| 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("
|
| 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),
|
| 152 |
-
gr.update(),
|
|
|
|
|
|
|
| 153 |
reasoning,
|
| 154 |
-
"⚠️ Error: No tasks to plan! Please add tasks.",
|
| 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),
|
| 168 |
-
gr.update(visible=False),
|
| 169 |
-
gr.update(visible=True),
|
| 170 |
-
gr.update(selected="
|
|
|
|
|
|
|
| 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 |
-
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
return
|
| 185 |
|
| 186 |
iterator = self.service.run_step3_team(session)
|
|
|
|
|
|
|
| 187 |
for event in iterator:
|
|
|
|
| 188 |
current_session = event.get("session", session)
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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("
|
| 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=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
]
|
| 327 |
)
|
| 328 |
|
| 329 |
step3_event = step2_event.then(
|
| 330 |
fn=self.step3_wrapper,
|
| 331 |
inputs=[session_state],
|
| 332 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 268 |
yield {
|
| 269 |
"type": "stream",
|
| 270 |
-
"stream_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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 437 |
-
|
| 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 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
report_html = f"## 🎯 Planning Complete\n\n{report_content}"
|
| 464 |
-
yield {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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**:
|
| 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 展示區域
|
| 13 |
with gr.Group(visible=False) as team_area:
|
| 14 |
gr.Markdown("### 🤖 Agent Status")
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
#
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 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 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
<div class="stream-container">
|
| 12 |
-
<div class="stream-text"
|
| 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 |
|