Accept MCP flat payload in deck generator to stop silent-blank decks
generate_deck on the MCP server passes a flat spec (customer_name, title, workload_type, services, cost_summary, ...) — but from_spec only understood the proposal-spec YAML shape (metadata/summary/architecture/...). With no matching keys, only the title and closing layouts rendered, both populated with empty strings, producing a deck that looked blank. Detect the flat shape in from_spec and adapt it into the proposal-spec fields that the existing slide builders already consume. Title, summary (target state + current-state bullets) and cost slides now render from whatever the MCP tool sends; the richer proposal-spec path is untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,86 @@ from pptx.enum.shapes import MSO_SHAPE
|
|||||||
from oci_pptx_base import Colors, Layouts, OraclePresBase
|
from oci_pptx_base import Colors, Layouts, OraclePresBase
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MCP flat-spec adapter
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def _is_flat_spec(spec: dict) -> bool:
|
||||||
|
"""MCP payload shape: no 'metadata' key, but has customer_name/id/title."""
|
||||||
|
if not isinstance(spec, dict):
|
||||||
|
return False
|
||||||
|
if "metadata" in spec:
|
||||||
|
return False
|
||||||
|
return any(k in spec for k in ("customer_name", "customer_id", "title"))
|
||||||
|
|
||||||
|
|
||||||
|
def _adapt_flat_spec(spec: dict) -> dict:
|
||||||
|
"""Map the MCP flat payload to the proposal-spec structure from_spec expects."""
|
||||||
|
arch = spec.get("architecture") if isinstance(spec.get("architecture"), dict) else {}
|
||||||
|
|
||||||
|
customer = spec.get("customer_name") or spec.get("customer_id", "")
|
||||||
|
title = spec.get("title") or arch.get("workload", "") or "Architecture Proposal"
|
||||||
|
workload = spec.get("workload_type") or arch.get("workload", "")
|
||||||
|
deployment = spec.get("deployment_model") or arch.get("deployment", "")
|
||||||
|
region = spec.get("primary_region") or spec.get("region", "")
|
||||||
|
billing = spec.get("billing_model") or arch.get("license_model", "")
|
||||||
|
dr_raw = spec.get("disaster_recovery")
|
||||||
|
if dr_raw is None and "dr" in arch:
|
||||||
|
dr_raw = "Enabled" if arch.get("dr") else "Not in scope"
|
||||||
|
capacity = spec.get("capacity") or arch.get("capacity") or {}
|
||||||
|
|
||||||
|
target_parts = [p for p in (workload, deployment and f"deployment: {deployment}",
|
||||||
|
region and f"region: {region}") if p]
|
||||||
|
target_state = " — ".join(target_parts)
|
||||||
|
|
||||||
|
current_state = []
|
||||||
|
if isinstance(capacity, dict):
|
||||||
|
for k, v in capacity.items():
|
||||||
|
current_state.append(f"{k.replace('_', ' ').title()}: {v}")
|
||||||
|
if billing:
|
||||||
|
current_state.append(f"License model: {billing}")
|
||||||
|
if dr_raw:
|
||||||
|
current_state.append(f"Disaster recovery: {dr_raw}")
|
||||||
|
|
||||||
|
line_items = []
|
||||||
|
for svc in spec.get("services", []) or []:
|
||||||
|
if not isinstance(svc, dict):
|
||||||
|
continue
|
||||||
|
qty = svc.get("quantity", svc.get("qty"))
|
||||||
|
notes = svc.get("notes", "")
|
||||||
|
if qty is not None:
|
||||||
|
notes = f"Qty: {qty}" + (f" — {notes}" if notes else "")
|
||||||
|
line_items.append({"component": svc.get("name", ""), "monthly_payg": "—", "notes": notes})
|
||||||
|
|
||||||
|
cost_summary = spec.get("cost_summary") or {}
|
||||||
|
monthly = cost_summary.get("monthly_estimate")
|
||||||
|
if monthly is not None:
|
||||||
|
currency = cost_summary.get("currency", "USD")
|
||||||
|
annual = cost_summary.get("annual_estimate", monthly * 12)
|
||||||
|
line_items.append({
|
||||||
|
"component": "Total (estimated)",
|
||||||
|
"monthly_payg": f"{currency} {monthly:,.2f}",
|
||||||
|
"notes": f"Annual: {currency} {annual:,.2f}",
|
||||||
|
})
|
||||||
|
|
||||||
|
adapted = {
|
||||||
|
"metadata": {"customer": customer, "subtitle": title},
|
||||||
|
"summary": {
|
||||||
|
"why": f"{customer} — {title}" if customer and title else (customer or title),
|
||||||
|
"current_state": current_state,
|
||||||
|
"target_state": target_state,
|
||||||
|
"timeline": "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if line_items:
|
||||||
|
adapted["cost"] = {
|
||||||
|
"line_items": line_items,
|
||||||
|
"assumptions": spec.get("assumptions", []) or [],
|
||||||
|
"show_byol": False,
|
||||||
|
}
|
||||||
|
return adapted
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Slide Generator
|
# Slide Generator
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -1185,6 +1265,13 @@ class OCIDeckGenerator(OraclePresBase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_spec(cls, spec: dict, template: Optional[str] = None) -> "OCIDeckGenerator":
|
def from_spec(cls, spec: dict, template: Optional[str] = None) -> "OCIDeckGenerator":
|
||||||
"""Build a complete deck from a YAML specification."""
|
"""Build a complete deck from a YAML specification."""
|
||||||
|
# The MCP server passes a flat payload shape (customer_name, services,
|
||||||
|
# cost_summary, ...) that doesn't match the proposal-spec structure this
|
||||||
|
# method was originally written against. Adapt it so the deck renders
|
||||||
|
# with actual content instead of only blank title/closing slides.
|
||||||
|
if _is_flat_spec(spec):
|
||||||
|
spec = _adapt_flat_spec(spec)
|
||||||
|
|
||||||
meta = spec.get("metadata", {})
|
meta = spec.get("metadata", {})
|
||||||
gen = cls(
|
gen = cls(
|
||||||
customer=meta.get("customer", ""),
|
customer=meta.get("customer", ""),
|
||||||
|
|||||||
Reference in New Issue
Block a user