2 Commits

Author SHA1 Message Date
root
bcb16795d1 Add BOM cookbook to shortcut SKU mapping for common patterns
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 24s
Codex sessions burn 10+ tool calls grepping kb/field-knowledge and the SKU
catalog to map standard requirements (ExaCS X11M BYOL, ADB-D, ADB-S + block
+ FastConnect) to the 4-5 SKUs the MCP BOM tools need. Bake the recipes
into docs/bom-cookbook.md so the LLM can skip the exploration loop for
well-known patterns, and link it from SKILL.md between Output Generation
and Knowledge Base (same progressive-disclosure pattern the other docs use).

Recipe 2 (ADB-Dedicated) explicitly calls out the infra-SKU overlap with
ExaCS — one of the specific traps the exploration loop keeps hitting.
Catalog-fallback note keeps the cookbook honest when a requirement does
not match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:29:20 -03:00
root
e57ab57b48 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>
2026-04-20 15:27:44 -03:00
3 changed files with 240 additions and 0 deletions

View File

@@ -432,6 +432,14 @@ See [docs/skill/output-formats.md](docs/skill/output-formats.md) for the per-cus
--- ---
## Cookbook: Building Tool Payloads
Before mapping customer requirements to SKUs for `generate_bom`, `generate_bom_appca`, or `generate_cost_estimate`, check [docs/bom-cookbook.md](docs/bom-cookbook.md) — it has copy-paste recipes for common patterns (ExaCS X11M BYOL, ADB-Dedicated, ADB-S + Block + FastConnect) and explicitly names the gotchas that otherwise burn exploration turns (e.g. ADB-Dedicated shares infrastructure SKUs with ExaCS — there are no separate `adb_dedicated` SKUs in the catalog).
If a requirement does not match a recipe, `grep` [`kb/pricing/oci-sku-catalog.yaml`](kb/pricing/oci-sku-catalog.yaml) and consult `kb/services/` before inventing SKUs.
---
## Knowledge Base ## Knowledge Base
KB lives under `kb/`. See [kb/README.md](kb/README.md) for the directory map, frontmatter requirements, refresh tooling, and contributor guide. KB lives under `kb/`. See [kb/README.md](kb/README.md) for the directory map, frontmatter requirements, refresh tooling, and contributor guide.

145
docs/bom-cookbook.md Normal file
View File

@@ -0,0 +1,145 @@
# BOM Cookbook — Building `generate_bom` Payloads
Copy-paste recipes that map common customer requirements to the exact OCI SKUs the
`generate_bom`, `generate_bom_appca`, and `generate_cost_estimate` MCP tools expect.
The goal is to skip the "grep the catalog and guess SKUs" exploration loop that
burns tool calls on well-known patterns.
> **If the requirement does not match any recipe below, consult
> [`kb/services/`](../kb/services/) and
> [`kb/pricing/oci-sku-catalog.yaml`](../kb/pricing/oci-sku-catalog.yaml)
> before inventing SKUs.** All SKUs below are verified against the catalog as of
> 2026-04-20; re-`grep` the catalog if a recipe ever fails.
## Payload Shape
All three tools accept the same envelope:
```json
{
"customer_id": "acme-prod",
"discount_pct": 0.35,
"currency": "USD",
"services": [
{ "sku": "B90777", "quantity": 1 }
]
}
```
- `customer_id`: free-form identifier, used for output file naming.
- `discount_pct`: decimal (0.35 = 35%). Applied to every line unless overridden.
- `services[].quantity`: integer or float depending on the SKU metric (ECPUs, GB, hours).
---
## Recipe 1 — Exadata Cloud Service (ExaCS) X11M, BYOL
**When it matches:** "ExaCS X11M BYOL, N ECPUs, M TB storage, no DR" — customer-managed
Oracle Database on dedicated Exadata hardware, BYOL licensing.
**SKUs:**
| SKU | Component | Typical qty |
|-----|-----------|-------------|
| `B90777` | Exadata Infrastructure — Base System | `1` per rack |
| `B110627` | Exadata Database Server — X11M | `2` (Base/Quarter) — adjust per rack config |
| `B110629` | Exadata Storage Server — X11M | `3` (Base/Quarter) — adjust per rack config |
| `B110632` | Exadata Database ECPU — Dedicated Infra BYOL (X11M) | One per ECPU |
Swap `B110632``B110631` for PAYG (license-included) pricing.
**Payload (120 ECPUs, 120 TB, BYOL, standard Base System):**
```json
{
"customer_id": "acme-exacs",
"discount_pct": 0.35,
"currency": "USD",
"services": [
{ "sku": "B90777", "quantity": 1 },
{ "sku": "B110627", "quantity": 2 },
{ "sku": "B110629", "quantity": 3 },
{ "sku": "B110632", "quantity": 120 }
]
}
```
---
## Recipe 2 — Autonomous Database Dedicated (ADB-D), BYOL
**When it matches:** "ADB Dedicated, N ECPUs, M TB, BYOL" — Autonomous Database running
on dedicated Exadata infrastructure.
**⚠️ Gotcha that burns tool calls:** ADB-Dedicated has **no separate infrastructure
SKUs** in the catalog. It reuses the same Exadata dedicated infra SKUs as ExaCS
(`B90777`, `B110627`, `B110629`) plus the same Exadata ECPU SKU (`B110631`/`B110632`).
The Autonomous-vs-customer-managed distinction is a tenancy-level configuration,
not a billing line item. Do not search for `autonomous_dedicated` SKUs — they do
not exist.
**SKUs:** identical to Recipe 1.
**Payload (64 ECPUs, BYOL):**
```json
{
"customer_id": "acme-adbd",
"discount_pct": 0.35,
"currency": "USD",
"services": [
{ "sku": "B90777", "quantity": 1 },
{ "sku": "B110627", "quantity": 2 },
{ "sku": "B110629", "quantity": 3 },
{ "sku": "B110632", "quantity": 64 }
]
}
```
---
## Recipe 3 — Autonomous Database Serverless (ADB-S) + Block Volume + FastConnect
**When it matches:** "ADB-S ATP, N ECPUs, M TB data + block storage for app tier +
FastConnect to on-prem" — the typical "app on OCI Compute, DB on ADB-S" pattern.
**SKUs:**
| SKU | Component | Metric |
|-----|-----------|--------|
| `B95701` | ADB ECPU (ATP or ADW) — license-included | ECPU/hour |
| `B95703` | ADB ECPU BYOL — use instead of B95701 if BYOL | ECPU/hour |
| `B95706` | ADB ATP data storage | GB/month |
| `B95754` | ADB ADW data storage — use instead of B95706 for warehouse | GB/month |
| `B95754a` | ADB backup storage | GB/month |
| `B91961` | Block Volume storage (app tier) | GB/month |
| `B91962` | Block Volume performance (VPU) — optional, for higher IOPS tiers | VPU/GB/month |
| `B88326` | FastConnect 10 Gbps port | port-hour |
Swap `B88326``B88325` (1 Gbps) or `B93126` (100 Gbps) as needed.
**Payload (16 ECPUs ATP license-included, 2 TB data + 1 TB backup, 500 GB block, 10 G FastConnect):**
```json
{
"customer_id": "acme-adbs",
"discount_pct": 0.35,
"currency": "USD",
"services": [
{ "sku": "B95701", "quantity": 16 },
{ "sku": "B95706", "quantity": 2048 },
{ "sku": "B95754a", "quantity": 1024 },
{ "sku": "B91961", "quantity": 500 },
{ "sku": "B88326", "quantity": 1 }
]
}
```
---
## When a recipe doesn't fit
1. Grep the catalog: `grep -n "<product keyword>" kb/pricing/oci-sku-catalog.yaml`
2. Check service docs under `kb/services/` for deployment-specific guidance.
3. Check `kb/field-knowledge/` for known sizing/pricing gotchas.
4. If a SKU still cannot be located, surface the gap to the user rather than guessing — fabricated SKUs break the BOM silently.

View File

@@ -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", ""),