split each MCP server into tools/resources/prompts modules
This commit is contained in:
36
mcp_servers/passenger/prompts.py
Normal file
36
mcp_servers/passenger/prompts.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Prompts for the passenger MCP server."""
|
||||
|
||||
from mcp_servers.passenger.server import mcp
|
||||
|
||||
TONE_INSTRUCTIONS = {
|
||||
"empathetic": (
|
||||
"Write a passenger notification about this flight disruption. "
|
||||
"Be empathetic and human. Explain WHY the delay/cancellation happened "
|
||||
"using the weather, maintenance, or operational data provided. "
|
||||
"Tell the passenger what's happening next: new boarding time, gate, "
|
||||
"rebooking options. End with reassurance. No jargon."
|
||||
),
|
||||
"factual": (
|
||||
"Write a brief, factual notification. Include: flight number, "
|
||||
"route, status, delay duration, cause (one phrase), new departure time, "
|
||||
"gate. No editorial. No reassurance. Just facts."
|
||||
),
|
||||
"brief_sms": (
|
||||
"Write an SMS-length notification (under 160 characters). "
|
||||
"Format: UA{flight} {route}: {status}. {cause}. New dep: {time}. Gate {gate}."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@mcp.prompt()
|
||||
def passenger_notification(tone: str = "empathetic") -> str:
|
||||
"""Template for generating passenger delay/cancellation notifications.
|
||||
|
||||
tone: empathetic (default) | factual | brief_sms
|
||||
"""
|
||||
instruction = TONE_INSTRUCTIONS.get(tone, TONE_INSTRUCTIONS["empathetic"])
|
||||
return (
|
||||
f"{instruction}\n\n"
|
||||
"Use the operational data provided below. Do not invent details. "
|
||||
"If data is missing for a section, omit that section."
|
||||
)
|
||||
49
mcp_servers/passenger/resources.py
Normal file
49
mcp_servers/passenger/resources.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Resources for the passenger MCP server."""
|
||||
|
||||
import json
|
||||
|
||||
from mcp_servers.data.scenarios.manager import scenario_manager
|
||||
from mcp_servers.passenger.server import mcp
|
||||
|
||||
|
||||
@mcp.resource("ops://flights/{flight_id}/manifest")
|
||||
def flight_manifest(flight_id: str) -> str:
|
||||
"""Passenger manifest summary for a flight.
|
||||
|
||||
Returns: flight_id, passenger count, count by MP status tier,
|
||||
special needs count, connection count.
|
||||
Summary-level — no PII exposed through this resource.
|
||||
"""
|
||||
flight = None
|
||||
for f in scenario_manager.flights:
|
||||
if f.flight_id == flight_id:
|
||||
flight = f
|
||||
break
|
||||
|
||||
if not flight:
|
||||
return json.dumps({"error": f"Flight {flight_id} not found"})
|
||||
|
||||
pax_on_flight = [
|
||||
p for p in scenario_manager.passengers
|
||||
if p.flight_id == flight_id
|
||||
]
|
||||
|
||||
status_counts = {}
|
||||
special_needs_count = 0
|
||||
connection_count = 0
|
||||
|
||||
for p in pax_on_flight:
|
||||
status = p.mileage_plus_status.value
|
||||
status_counts[status] = status_counts.get(status, 0) + 1
|
||||
if p.special_needs:
|
||||
special_needs_count += 1
|
||||
if p.connection_flight:
|
||||
connection_count += 1
|
||||
|
||||
return json.dumps({
|
||||
"flight_id": flight_id,
|
||||
"total_passengers": flight.passenger_count,
|
||||
"by_status": status_counts,
|
||||
"special_needs_count": special_needs_count,
|
||||
"connection_count": connection_count,
|
||||
})
|
||||
@@ -4,13 +4,8 @@ Covers: passenger notification generation, flight manifest, and
|
||||
notification prompt template (multi-tone).
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from mcp_servers.data.models import MPStatus
|
||||
from mcp_servers.data.scenarios.manager import scenario_manager
|
||||
|
||||
mcp = FastMCP(
|
||||
"stellar-ops-passenger",
|
||||
instructions=(
|
||||
@@ -19,151 +14,4 @@ mcp = FastMCP(
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ── Tools ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def generate_notification(context: dict) -> str:
|
||||
"""Synthesizes flight disruption context into an empathetic,
|
||||
actionable passenger notification.
|
||||
|
||||
Uses Claude via Anthropic SDK (or Bedrock when USE_BEDROCK=true).
|
||||
Output: clear, human, no jargon, includes gate/time/status.
|
||||
Falls back to template if no API key is configured.
|
||||
"""
|
||||
try:
|
||||
from mcp_servers.shared_llm import generate, _get_provider
|
||||
|
||||
system_prompt = (
|
||||
"You are a passenger notification system for Stellar Air. "
|
||||
"Write a clear, empathetic notification about this flight disruption. "
|
||||
"Explain WHY the delay or cancellation happened using the operational data provided. "
|
||||
"Tell the passenger what's happening next: new boarding time, gate, status. "
|
||||
"Be human and reassuring. No aviation jargon. No speculation. "
|
||||
"If data is missing for a section, omit it — don't make things up."
|
||||
)
|
||||
text = await generate(system_prompt, json.dumps(context, indent=2))
|
||||
return json.dumps({"text": text, "provider": _get_provider()})
|
||||
except Exception:
|
||||
return json.dumps({"text": _template_notification(context), "provider": "template"})
|
||||
|
||||
|
||||
def _template_notification(context: dict) -> str:
|
||||
"""Structured template fallback when LLM is unavailable."""
|
||||
flight_id = context.get("flight_id", "")
|
||||
origin = context.get("origin", "")
|
||||
destination = context.get("destination", "")
|
||||
status = context.get("status", "DELAYED")
|
||||
delay_minutes = context.get("delay_minutes", 0)
|
||||
delay_cause = context.get("delay_cause", "")
|
||||
gate = context.get("gate", "")
|
||||
weather_summary = context.get("weather_summary", "")
|
||||
crew_notes_summary = context.get("crew_notes_summary", "")
|
||||
|
||||
lines = [
|
||||
f"{flight_id} — {origin} → {destination}",
|
||||
f"Status: {status}" + (f" {delay_minutes} minutes" if delay_minutes else ""),
|
||||
"",
|
||||
]
|
||||
|
||||
if delay_cause:
|
||||
cause_text = {
|
||||
"WEATHER": "weather conditions along your route",
|
||||
"MAINTENANCE": "a routine maintenance check on your aircraft",
|
||||
"CREW": "ensuring your crew is fully rested for safe operation",
|
||||
"ATC": "air traffic control restrictions",
|
||||
"LATE_AIRCRAFT": "the late arrival of your inbound aircraft",
|
||||
}.get(delay_cause, f"{delay_cause.lower()}")
|
||||
lines.append(f"Your flight is delayed due to {cause_text}.")
|
||||
if weather_summary:
|
||||
lines.append(f"Current conditions: {weather_summary}.")
|
||||
|
||||
if gate:
|
||||
lines.append(f"\nGate {gate} — no gate change expected.")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ── Resources ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@mcp.resource("ops://flights/{flight_id}/manifest")
|
||||
def flight_manifest(flight_id: str) -> str:
|
||||
"""Passenger manifest summary for a flight.
|
||||
|
||||
Returns: flight_id, passenger count, count by MP status tier,
|
||||
special needs count, connection count.
|
||||
Summary-level — no PII exposed through this resource.
|
||||
"""
|
||||
flight = None
|
||||
for f in scenario_manager.flights:
|
||||
if f.flight_id == flight_id:
|
||||
flight = f
|
||||
break
|
||||
|
||||
if not flight:
|
||||
return json.dumps({"error": f"Flight {flight_id} not found"})
|
||||
|
||||
# Count passengers by status from scenario data
|
||||
pax_on_flight = [
|
||||
p for p in scenario_manager.passengers
|
||||
if p.flight_id == flight_id
|
||||
]
|
||||
|
||||
status_counts = {}
|
||||
special_needs_count = 0
|
||||
connection_count = 0
|
||||
|
||||
for p in pax_on_flight:
|
||||
status = p.mileage_plus_status.value
|
||||
status_counts[status] = status_counts.get(status, 0) + 1
|
||||
if p.special_needs:
|
||||
special_needs_count += 1
|
||||
if p.connection_flight:
|
||||
connection_count += 1
|
||||
|
||||
return json.dumps({
|
||||
"flight_id": flight_id,
|
||||
"total_passengers": flight.passenger_count,
|
||||
"by_status": status_counts,
|
||||
"special_needs_count": special_needs_count,
|
||||
"connection_count": connection_count,
|
||||
})
|
||||
|
||||
|
||||
# ── Prompts ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
TONE_INSTRUCTIONS = {
|
||||
"empathetic": (
|
||||
"Write a passenger notification about this flight disruption. "
|
||||
"Be empathetic and human. Explain WHY the delay/cancellation happened "
|
||||
"using the weather, maintenance, or operational data provided. "
|
||||
"Tell the passenger what's happening next: new boarding time, gate, "
|
||||
"rebooking options. End with reassurance. No jargon."
|
||||
),
|
||||
"factual": (
|
||||
"Write a brief, factual notification. Include: flight number, "
|
||||
"route, status, delay duration, cause (one phrase), new departure time, "
|
||||
"gate. No editorial. No reassurance. Just facts."
|
||||
),
|
||||
"brief_sms": (
|
||||
"Write an SMS-length notification (under 160 characters). "
|
||||
"Format: UA{flight} {route}: {status}. {cause}. New dep: {time}. Gate {gate}."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@mcp.prompt()
|
||||
def passenger_notification(tone: str = "empathetic") -> str:
|
||||
"""Template for generating passenger delay/cancellation notifications.
|
||||
|
||||
tone: empathetic (default) | factual | brief_sms
|
||||
"""
|
||||
instruction = TONE_INSTRUCTIONS.get(tone, TONE_INSTRUCTIONS["empathetic"])
|
||||
return (
|
||||
f"{instruction}\n\n"
|
||||
"Use the operational data provided below. Do not invent details. "
|
||||
"If data is missing for a section, omit that section."
|
||||
)
|
||||
from mcp_servers.passenger import tools, resources, prompts # noqa: E402, F401
|
||||
|
||||
66
mcp_servers/passenger/tools.py
Normal file
66
mcp_servers/passenger/tools.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Tools for the passenger MCP server."""
|
||||
|
||||
import json
|
||||
|
||||
from mcp_servers.passenger.server import mcp
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def generate_notification(context: dict) -> str:
|
||||
"""Synthesizes flight disruption context into an empathetic,
|
||||
actionable passenger notification.
|
||||
|
||||
Uses Claude via Anthropic SDK (or Bedrock when USE_BEDROCK=true).
|
||||
Output: clear, human, no jargon, includes gate/time/status.
|
||||
Falls back to template if no API key is configured.
|
||||
"""
|
||||
try:
|
||||
from mcp_servers.shared_llm import generate, _get_provider
|
||||
|
||||
system_prompt = (
|
||||
"You are a passenger notification system for Stellar Air. "
|
||||
"Write a clear, empathetic notification about this flight disruption. "
|
||||
"Explain WHY the delay or cancellation happened using the operational data provided. "
|
||||
"Tell the passenger what's happening next: new boarding time, gate, status. "
|
||||
"Be human and reassuring. No aviation jargon. No speculation. "
|
||||
"If data is missing for a section, omit it — don't make things up."
|
||||
)
|
||||
text = await generate(system_prompt, json.dumps(context, indent=2))
|
||||
return json.dumps({"text": text, "provider": _get_provider()})
|
||||
except Exception:
|
||||
return json.dumps({"text": _template_notification(context), "provider": "template"})
|
||||
|
||||
|
||||
def _template_notification(context: dict) -> str:
|
||||
"""Structured template fallback when LLM is unavailable."""
|
||||
flight_id = context.get("flight_id", "")
|
||||
origin = context.get("origin", "")
|
||||
destination = context.get("destination", "")
|
||||
status = context.get("status", "DELAYED")
|
||||
delay_minutes = context.get("delay_minutes", 0)
|
||||
delay_cause = context.get("delay_cause", "")
|
||||
gate = context.get("gate", "")
|
||||
weather_summary = context.get("weather_summary", "")
|
||||
|
||||
lines = [
|
||||
f"{flight_id} — {origin} → {destination}",
|
||||
f"Status: {status}" + (f" {delay_minutes} minutes" if delay_minutes else ""),
|
||||
"",
|
||||
]
|
||||
|
||||
if delay_cause:
|
||||
cause_text = {
|
||||
"WEATHER": "weather conditions along your route",
|
||||
"MAINTENANCE": "a routine maintenance check on your aircraft",
|
||||
"CREW": "ensuring your crew is fully rested for safe operation",
|
||||
"ATC": "air traffic control restrictions",
|
||||
"LATE_AIRCRAFT": "the late arrival of your inbound aircraft",
|
||||
}.get(delay_cause, f"{delay_cause.lower()}")
|
||||
lines.append(f"Your flight is delayed due to {cause_text}.")
|
||||
if weather_summary:
|
||||
lines.append(f"Current conditions: {weather_summary}.")
|
||||
|
||||
if gate:
|
||||
lines.append(f"\nGate {gate} — no gate change expected.")
|
||||
|
||||
return "\n".join(lines)
|
||||
Reference in New Issue
Block a user