Spaces:
Running
feat: 🚀 Evolve to Stateful MCP Architecture with Context Injection Middleware
Browse filesMajor architectural overhaul to decouple Agent Logic (Service) from Tool Execution (Server) using the Model Context Protocol (MCP).
Key Highlights:
- **Micro-Architecture**: Migrated local toolkits to a standalone `FastMCP` server process (`mcp_server_lifeflow.py`).
- **Context Engineering**: Implemented an AOP-style interceptor using Python `ContextVars`. This automatically injects `session_id` and `api_key` into MCP tool calls, keeping Agent prompts clean and reducing token usage.
- **Multi-Tenant Safety**: Achieved strict session isolation. User-provided API keys dynamically override system environment variables via the injection layer.
- **Runtime Metaprogramming**: Solved Agno/Phidata internal naming conflicts (`_` prefix) by dynamically patching tool entrypoints at startup.
- **Exclusive Channels**: Configured dedicated MCP pipes for each Agent (`Scout`, `Navigator`, etc.) to prevent tool hallucination.
This commit satisfies all requirements for Hackathon Track 2: MCP in Action.
- README.md +13 -0
- mcp_server_lifeflow.py +176 -0
|
@@ -21,6 +21,19 @@ tags:
|
|
| 21 |
|
| 22 |
---
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
## 📖 Overview
|
| 25 |
|
| 26 |
**LifeFlow AI** is not just a chatbot; it's a **State Machine for Real-World Operations**. It solves the complexity of daily travel planning—considering traffic, weather, opening hours, and route optimization—by coordinating a team of specialized AI agents.
|
|
|
|
| 21 |
|
| 22 |
---
|
| 23 |
|
| 24 |
+
## 👥 Team
|
| 25 |
+
|
| 26 |
+
### Team Information:
|
| 27 |
+
- **Man-Ho Li** - [@Marco310](https://huggingface.co/Marco310) - Lead Developer & AI Architect
|
| 28 |
+
- ** ** - [@]() - Technical Consultant & Marketing Director
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
## 📺 Demo & Submission
|
| 32 |
+
- Demo Video: [Coming Soon]
|
| 33 |
+
- Social Media Post: [Coming Soon]
|
| 34 |
+
- Submission Date: November 2025
|
| 35 |
+
|
| 36 |
+
|
| 37 |
## 📖 Overview
|
| 38 |
|
| 39 |
**LifeFlow AI** is not just a chatbot; it's a **State Machine for Real-World Operations**. It solves the complexity of daily travel planning—considering traffic, weather, opening hours, and route optimization—by coordinating a team of specialized AI agents.
|
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mcp_server_lifeflow.py
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
from fastmcp import FastMCP
|
| 5 |
+
|
| 6 |
+
# 引入你原本的工具包 (保持邏輯不變)
|
| 7 |
+
from src.tools.scout_toolkit import ScoutToolkit
|
| 8 |
+
from src.tools.optimizer_toolkit import OptimizationToolkit
|
| 9 |
+
from src.tools.navigation_toolkit import NavigationToolkit
|
| 10 |
+
from src.tools.weather_toolkit import WeatherToolkit
|
| 11 |
+
from src.tools.reader_toolkit import ReaderToolkit
|
| 12 |
+
|
| 13 |
+
from src.infra.context import set_session_id # 記得 import 這個
|
| 14 |
+
|
| 15 |
+
# 定義 Server
|
| 16 |
+
mcp = FastMCP("LifeFlow Core Service")
|
| 17 |
+
|
| 18 |
+
# ================= Helper: 獲取環境變數 =================
|
| 19 |
+
# 注意:這些 Key 是由 app.py 在啟動 subprocess 時透過 env 傳進來的
|
| 20 |
+
def get_env_key(name):
|
| 21 |
+
return os.environ.get(name)
|
| 22 |
+
|
| 23 |
+
# ================= Tool Definitions =================
|
| 24 |
+
|
| 25 |
+
@mcp.tool()
|
| 26 |
+
def search_and_offload(task_list_json: str, session_id: str = None) -> str:
|
| 27 |
+
"""
|
| 28 |
+
Performs a proximity search for POIs based on the provided tasks and global context, then offloads results to the DB.
|
| 29 |
+
|
| 30 |
+
CRITICAL: The input JSON **MUST** include the 'global_info' section containing 'start_location' (lat, lng) to ensure searches are performed nearby the user's starting point, not in a random location.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
task_list_json (str): A JSON formatted string. The structure must be:
|
| 34 |
+
{
|
| 35 |
+
"global_info": {
|
| 36 |
+
"language": str,
|
| 37 |
+
"plan_type": str
|
| 38 |
+
"return_to_start": bool,
|
| 39 |
+
"start_location": ...,
|
| 40 |
+
"departure_time": str,
|
| 41 |
+
"deadline": str or null,
|
| 42 |
+
|
| 43 |
+
},
|
| 44 |
+
"tasks": [
|
| 45 |
+
{
|
| 46 |
+
"task_id": 1,
|
| 47 |
+
"category": "MEAL" | "LEISURE" | "ERRAND" | "SHOPPING",
|
| 48 |
+
"description": "Short description",
|
| 49 |
+
"location_hint": "Clean Keyword for Google Maps",
|
| 50 |
+
"priority": "HIGH" | "MEDIUM" | "LOW",
|
| 51 |
+
"service_duration_min": 30,
|
| 52 |
+
"time_window": {
|
| 53 |
+
"earliest_time": "ISO 8601" or null,
|
| 54 |
+
"latest_time": "ISO 8601" or null}
|
| 55 |
+
}
|
| 56 |
+
...,
|
| 57 |
+
]
|
| 58 |
+
}
|
| 59 |
+
Returns:
|
| 60 |
+
str: A Ref_id of DB system.
|
| 61 |
+
"""
|
| 62 |
+
if session_id: set_session_id(session_id)
|
| 63 |
+
|
| 64 |
+
key = get_env_key("GOOGLE_MAPS_API_KEY")
|
| 65 |
+
# 實例化原本的 Toolkit
|
| 66 |
+
tool = ScoutToolkit(google_maps_api_key=key)
|
| 67 |
+
# 執行業務邏輯
|
| 68 |
+
return tool.search_and_offload(task_list_json)
|
| 69 |
+
|
| 70 |
+
@mcp.tool()
|
| 71 |
+
def optimize_from_ref(ref_id: str, max_wait_time_min: int = 0, return_to_start: bool = None, session_id: str = None) -> str:
|
| 72 |
+
"""
|
| 73 |
+
Executes the mathematical route solver (TSPTW) to find the most efficient task sequence.
|
| 74 |
+
|
| 75 |
+
This tool loads the POI data, calculates travel times, and reorders tasks to respect time windows.
|
| 76 |
+
It returns detailed status information, allowing the caller to decide if a retry (with relaxed constraints) is needed.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
ref_id (str): The unique reference ID returned by the Scout Agent.
|
| 80 |
+
max_wait_time_min (int, optional): Max waiting time allowed at a location. Defaults to 0.
|
| 81 |
+
return_to_start (bool, optional): Whether to force a round trip. Defaults to None. If None, it defaults to the value in 'global_info.return_to_start'.
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
str: A JSON string containing the result status and reference ID.
|
| 85 |
+
Structure:
|
| 86 |
+
{
|
| 87 |
+
"status": "OPTIMAL" | "FEASIBLE" | "INFEASIBLE" | "ERROR",
|
| 88 |
+
"opt_ref_id": str,
|
| 89 |
+
"dropped_tasks_count": int,
|
| 90 |
+
"skipped_tasks_id": list,
|
| 91 |
+
"message": str
|
| 92 |
+
}
|
| 93 |
+
"""
|
| 94 |
+
if session_id: set_session_id(session_id)
|
| 95 |
+
|
| 96 |
+
tool = OptimizationToolkit()
|
| 97 |
+
return tool.optimize_from_ref(ref_id, max_wait_time_min, return_to_start)
|
| 98 |
+
|
| 99 |
+
@mcp.tool()
|
| 100 |
+
def calculate_traffic_and_timing(optimization_ref_id: str, session_id: str = None) -> str:
|
| 101 |
+
"""
|
| 102 |
+
Calculates precise travel times, traffic delays, and arrival timestamps for the optimized route.
|
| 103 |
+
This tool acts as a 'Reality Check' by applying Google Routes data to the sequence generated by the Optimizer.
|
| 104 |
+
It ensures the schedule is physically possible under current traffic conditions and generates the final timeline.
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
optimization_ref_id (str): The unique reference ID returned by the Optimizer Agent (e.g., "optimization_result_xyz").
|
| 108 |
+
This ID links to the logically sorted task list.
|
| 109 |
+
|
| 110 |
+
Returns:
|
| 111 |
+
str: A JSON string containing the 'nav_ref_id' (e.g., '{"nav_ref_id": "navigation_result_abc"}').
|
| 112 |
+
"""
|
| 113 |
+
if session_id: set_session_id(session_id)
|
| 114 |
+
|
| 115 |
+
key = get_env_key("GOOGLE_MAPS_API_KEY")
|
| 116 |
+
tool = NavigationToolkit(google_maps_api_key=key)
|
| 117 |
+
return tool.calculate_traffic_and_timing(optimization_ref_id)
|
| 118 |
+
|
| 119 |
+
@mcp.tool()
|
| 120 |
+
def check_weather_for_timeline(nav_ref_id: str, session_id: str = None) -> str:
|
| 121 |
+
"""
|
| 122 |
+
Enriches the solved navigation route with weather forecasts and Air Quality Index (AQI) to create the final timeline.
|
| 123 |
+
|
| 124 |
+
This tool is the final post-processing step. It loads the solved route data, calculates precise local arrival times for each stop
|
| 125 |
+
(dynamically adjusting for the destination's timezone), and fetches specific weather conditions and AQI for those times.
|
| 126 |
+
It also resolves final location names and saves the complete itinerary for presentation.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
nav_ref_id (str): The unique reference ID returned by the Route Solver (or Navigation) step.
|
| 130 |
+
|
| 131 |
+
Returns:
|
| 132 |
+
str: A JSON string containing the reference ID for the finalized data.
|
| 133 |
+
Structure:
|
| 134 |
+
{
|
| 135 |
+
"final_ref_id": str
|
| 136 |
+
}
|
| 137 |
+
"""
|
| 138 |
+
if session_id: set_session_id(session_id)
|
| 139 |
+
|
| 140 |
+
key = get_env_key("OPENWEATHER_API_KEY")
|
| 141 |
+
tool = WeatherToolkit(openweather_api_key=key)
|
| 142 |
+
return tool.check_weather_for_timeline(nav_ref_id)
|
| 143 |
+
|
| 144 |
+
@mcp.tool()
|
| 145 |
+
def read_final_itinerary(ref_id: str, session_id: str = None) -> str:
|
| 146 |
+
"""
|
| 147 |
+
Retrieves the complete, enriched itinerary data for final presentation.
|
| 148 |
+
|
| 149 |
+
This tool acts as the 'Data Fetcher' for the Presenter. It loads the fully processed
|
| 150 |
+
trip plan (including Weather, Traffic, and Optimized Route) associated with the `ref_id`.
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
ref_id (str): The unique reference ID returned by the Weatherman Agent (e.g., "final_itinerary_xyz").
|
| 154 |
+
This ID links to the completed dataset ready for reporting.
|
| 155 |
+
|
| 156 |
+
Returns:
|
| 157 |
+
str: A structured JSON string containing the full trip details.
|
| 158 |
+
Structure:
|
| 159 |
+
{
|
| 160 |
+
"status": "COMPLETE" | "INCOMPLETE",
|
| 161 |
+
"global_info": { ... },
|
| 162 |
+
"traffic_summary": { "total_distance": ..., "total_drive_time": ... },
|
| 163 |
+
"schedule": [
|
| 164 |
+
{ "time": "10:00", "location": "...", "weather": "...", "air_quality": "..." },
|
| 165 |
+
...,
|
| 166 |
+
]
|
| 167 |
+
}
|
| 168 |
+
"""
|
| 169 |
+
if session_id: set_session_id(session_id)
|
| 170 |
+
|
| 171 |
+
tool = ReaderToolkit()
|
| 172 |
+
return tool.read_final_itinerary(ref_id)
|
| 173 |
+
|
| 174 |
+
if __name__ == "__main__":
|
| 175 |
+
# 啟動 MCP Server
|
| 176 |
+
mcp.run()
|