From 3a994f54a01a0f8c11dfb4ce19808dcb93333f01 Mon Sep 17 00:00:00 2001 From: Runzhou Li Date: Tue, 18 Aug 2026 14:38:33 -0400 Subject: [PATCH 1/5] Add AI playbooks and agent skills --- .../compare-crc-velo-assessments/SKILL.md | 38 ++ .../agents/openai.yaml | 4 + .../references/crosswalk.md | 20 + .../scripts/inventory_assessments.py | 57 +++ .../crc-assess-asset-portfolio-risk/SKILL.md | 34 ++ .../agents/openai.yaml | 4 + .../references/playbook.md | 34 ++ .../scripts/assess_asset_portfolio.py | 49 ++ .../crc-model-flood-insurance-loss/SKILL.md | 33 ++ .../agents/openai.yaml | 4 + .../references/playbook.md | 33 ++ .../scripts/model_flood_loss.py | 63 +++ .../skills/crc-screen-mortgage-flood/SKILL.md | 41 ++ .../agents/openai.yaml | 4 + .../references/playbook.md | 37 ++ .../scripts/screen_mortgage_flood.py | 79 ++++ .../velo-assess-company-climate-risk/SKILL.md | 34 ++ .../agents/openai.yaml | 4 + .../references/playbook.md | 29 ++ .../scripts/assess_company.py | 70 +++ .../SKILL.md | 33 ++ .../agents/openai.yaml | 4 + .../references/playbook.md | 21 + .../scripts/triage_insurability.py | 53 +++ .../velo-underwrite-property-climate/SKILL.md | 33 ++ .../agents/openai.yaml | 4 + .../references/playbook.md | 26 ++ .../scripts/underwrite_property.py | 69 +++ .github/workflows/validate-skills.yml | 41 ++ README.md | 195 ++++++-- ai-playbooks/docs/capability-matrix.md | 26 ++ ai-playbooks/docs/dogfood-verification.md | 58 +++ ai-playbooks/docs/gap-backlog.md | 38 ++ ai-playbooks/docs/product-strategy.md | 56 +++ .../examples/dogfood/verify_skills.py | 430 ++++++++++++++++++ ai-playbooks/examples/run-open-demo.sh | 75 +++ .../examples/velo-company-example.json | 22 + pipelines/jrc_flood_pipeline.py | 8 +- pipelines/overture_assets_pipeline.py | 168 +++++++ pyproject.toml | 5 + uv.lock | 34 +- 41 files changed, 2039 insertions(+), 31 deletions(-) create mode 100644 .agents/skills/compare-crc-velo-assessments/SKILL.md create mode 100644 .agents/skills/compare-crc-velo-assessments/agents/openai.yaml create mode 100644 .agents/skills/compare-crc-velo-assessments/references/crosswalk.md create mode 100644 .agents/skills/compare-crc-velo-assessments/scripts/inventory_assessments.py create mode 100644 .agents/skills/crc-assess-asset-portfolio-risk/SKILL.md create mode 100644 .agents/skills/crc-assess-asset-portfolio-risk/agents/openai.yaml create mode 100644 .agents/skills/crc-assess-asset-portfolio-risk/references/playbook.md create mode 100644 .agents/skills/crc-assess-asset-portfolio-risk/scripts/assess_asset_portfolio.py create mode 100644 .agents/skills/crc-model-flood-insurance-loss/SKILL.md create mode 100644 .agents/skills/crc-model-flood-insurance-loss/agents/openai.yaml create mode 100644 .agents/skills/crc-model-flood-insurance-loss/references/playbook.md create mode 100644 .agents/skills/crc-model-flood-insurance-loss/scripts/model_flood_loss.py create mode 100644 .agents/skills/crc-screen-mortgage-flood/SKILL.md create mode 100644 .agents/skills/crc-screen-mortgage-flood/agents/openai.yaml create mode 100644 .agents/skills/crc-screen-mortgage-flood/references/playbook.md create mode 100644 .agents/skills/crc-screen-mortgage-flood/scripts/screen_mortgage_flood.py create mode 100644 .agents/skills/velo-assess-company-climate-risk/SKILL.md create mode 100644 .agents/skills/velo-assess-company-climate-risk/agents/openai.yaml create mode 100644 .agents/skills/velo-assess-company-climate-risk/references/playbook.md create mode 100644 .agents/skills/velo-assess-company-climate-risk/scripts/assess_company.py create mode 100644 .agents/skills/velo-triage-portfolio-insurability/SKILL.md create mode 100644 .agents/skills/velo-triage-portfolio-insurability/agents/openai.yaml create mode 100644 .agents/skills/velo-triage-portfolio-insurability/references/playbook.md create mode 100644 .agents/skills/velo-triage-portfolio-insurability/scripts/triage_insurability.py create mode 100644 .agents/skills/velo-underwrite-property-climate/SKILL.md create mode 100644 .agents/skills/velo-underwrite-property-climate/agents/openai.yaml create mode 100644 .agents/skills/velo-underwrite-property-climate/references/playbook.md create mode 100644 .agents/skills/velo-underwrite-property-climate/scripts/underwrite_property.py create mode 100644 .github/workflows/validate-skills.yml create mode 100644 ai-playbooks/docs/capability-matrix.md create mode 100644 ai-playbooks/docs/dogfood-verification.md create mode 100644 ai-playbooks/docs/gap-backlog.md create mode 100644 ai-playbooks/docs/product-strategy.md create mode 100644 ai-playbooks/examples/dogfood/verify_skills.py create mode 100755 ai-playbooks/examples/run-open-demo.sh create mode 100644 ai-playbooks/examples/velo-company-example.json create mode 100644 pipelines/overture_assets_pipeline.py diff --git a/.agents/skills/compare-crc-velo-assessments/SKILL.md b/.agents/skills/compare-crc-velo-assessments/SKILL.md new file mode 100644 index 0000000..403103d --- /dev/null +++ b/.agents/skills/compare-crc-velo-assessments/SKILL.md @@ -0,0 +1,38 @@ +--- +name: compare-crc-velo-assessments +description: Reconcile an open CRC climate-risk assessment with its VELO/CDT enterprise twin and explain the capability lift without treating unlike metrics as equivalent. Use when comparing outputs, preparing a paired demo, creating a sales-engineering memo, validating CRC-to-VELO synergy, or identifying product/data gaps exposed by both runs. +--- + +# Compare CRC and VELO assessments + +Make the open/proprietary relationship credible by preserving both provenance and non-equivalence. + +## Demo bootstrap + +For a zero-credential file-handling smoke test, first run +`ai-playbooks/examples/run-open-demo.sh` to create the Overture/JRC CRC output, +then inventory its `pipeline_output/ai-playbooks/flood-loss.parquet` beside +`ai-playbooks/examples/velo-company-example.json`. Label the JSON synthetic and +do not imply that the targets align or that the exercise demonstrates live VELO +capability. A decision-grade paired comparison requires CRC and VELO outputs for +the same real target, with aligned dimensions documented below. + +## Workflow + +1. Inventory both artifacts and run `scripts/inventory_assessments.py` when CRC Parquet and VELO JSON are available. +2. Read [references/crosswalk.md](references/crosswalk.md) and create an alignment table for asset/entity, hazard/factor, pathway, horizon, metric, unit, spatial precision, and source version. +3. Classify every comparison as: + - **Directly comparable**: same business object and metric semantics. + - **Directionally comparable**: related decision signal with material method differences. + - **Capability lift only**: available on one side and not reducible to the other. + - **Not comparable**: alignment or semantics fail. +4. Explain divergences using coverage, resolution, asset identity, hazards, scenario, vulnerability functions, probability conventions, and aggregation before discussing numerical difference. +5. Produce a paired decision memo: what CRC establishes, what VELO/CDT adds, whether the decision changes, and what evidence remains missing. +6. Add newly observed workflow issues to `ai-playbooks/docs/gap-backlog.md` with a reproducible example and suggested acceptance test, assigned to the CRC or VELO/CDT side. + +## Messaging guardrails + +- Do not position CRC as intentionally crippled. Position it as transparent, extensible infrastructure and reproducible baseline. +- Do not imply every VELO/CDT result is currently computed by CRC. State that CRC is the target open foundation and integration is increasingly being materialized. +- Do not claim causation from score differences. +- Do not hide nulls, unmatched assets, warnings, or scenario mismatch. diff --git a/.agents/skills/compare-crc-velo-assessments/agents/openai.yaml b/.agents/skills/compare-crc-velo-assessments/agents/openai.yaml new file mode 100644 index 0000000..5acf9d7 --- /dev/null +++ b/.agents/skills/compare-crc-velo-assessments/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Compare CRC and VELO Assessments" + short_description: "Reconcile open and enterprise climate results" + default_prompt: "Use $compare-crc-velo-assessments to compare these CRC and VELO outputs and explain the capability lift." diff --git a/.agents/skills/compare-crc-velo-assessments/references/crosswalk.md b/.agents/skills/compare-crc-velo-assessments/references/crosswalk.md new file mode 100644 index 0000000..94adea4 --- /dev/null +++ b/.agents/skills/compare-crc-velo-assessments/references/crosswalk.md @@ -0,0 +1,20 @@ +# Assessment crosswalk + +| Dimension | CRC evidence | VELO/CDT evidence | Rule | +|---|---|---|---| +| Object | Caller asset ID and coordinate/H3 | VELO asset/company/index ID and ownership | Require an explicit mapping; address similarity alone is insufficient | +| Hazard | Hazard name and value semantics | Impact factor/index | Record mapping version; do not assume names imply identical models | +| Scenario | Canonical pathway/horizon, often historical for JRC | VELO pathway and decadal horizon | Historical and future scenarios are not directly comparable | +| Hazard value | Return-period exposure in source units | Usually not the same public response object | Never compare directly to DCR/VaR/CVaR | +| Impact | Explicit user/registry transform and units | VELO impact score/attribution | Directional unless model and units are proven equivalent | +| Risk | CRC VaR/CVaR from explicit outcomes/distributions | VELO DCR, expected impact, VaR/CVaR | Require identical loss basis, confidence, aggregation, and scenario for direct comparison | +| Spatial | H3 resolution and `spatial_match` | Proprietary asset/hazard resolution | Explain granularity as capability lift | +| Provenance | Canonical metadata, source cache/version, package version | API result and available generation/method metadata | Flag missing version/evidence fields on either side | + +## Recommended demo table + +| Question | CRC baseline | VELO/CDT twin | Decision effect | Confidence/limits | +|---|---|---|---|---| + +Fill one row per decision-relevant finding. Avoid a single “winner” score. + diff --git a/.agents/skills/compare-crc-velo-assessments/scripts/inventory_assessments.py b/.agents/skills/compare-crc-velo-assessments/scripts/inventory_assessments.py new file mode 100644 index 0000000..9c11ca0 --- /dev/null +++ b/.agents/skills/compare-crc-velo-assessments/scripts/inventory_assessments.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Inventory CRC Parquet and VELO JSON evidence without asserting metric equivalence.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + +import pyarrow.parquet as pq + + +def decode_metadata(raw: dict[bytes, bytes] | None) -> dict[str, Any]: + decoded: dict[str, Any] = {} + for key, value in (raw or {}).items(): + text = value.decode("utf-8", errors="replace") + try: + decoded[key.decode("utf-8", errors="replace")] = json.loads(text) + except json.JSONDecodeError: + decoded[key.decode("utf-8", errors="replace")] = text + return decoded + + +def parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--crc", required=True, type=Path, help="CRC evaluation Parquet") + p.add_argument("--velo", required=True, type=Path, help="VELO JSON export") + p.add_argument("--output", required=True, type=Path) + return p + + +def main() -> int: + args = parser().parse_args() + parquet = pq.ParquetFile(args.crc) + velo = json.loads(args.velo.read_text()) + result = { + "classification": "inventory_only", + "non_equivalence_warning": "CRC return-period hazard/impact values must not be compared directly with VELO DCR, expected impact, VaR, or CVaR without a proven semantic crosswalk.", + "crc": { + "path": str(args.crc), + "rows": parquet.metadata.num_rows, + "columns": parquet.schema_arrow.names, + "metadata": decode_metadata(parquet.schema_arrow.metadata), + }, + "velo": {"path": str(args.velo), "top_level_fields": sorted(velo), "evidence": velo}, + "alignment_required": ["asset/entity", "hazard/factor", "pathway", "horizon", "metric", "unit", "spatial precision", "source/model version"], + } + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n") + print(args.output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/.agents/skills/crc-assess-asset-portfolio-risk/SKILL.md b/.agents/skills/crc-assess-asset-portfolio-risk/SKILL.md new file mode 100644 index 0000000..43fec27 --- /dev/null +++ b/.agents/skills/crc-assess-asset-portfolio-risk/SKILL.md @@ -0,0 +1,34 @@ +--- +name: crc-assess-asset-portfolio-risk +description: Assess a user-supplied portfolio or Overture candidate locations against one or more CRC canonical hazard datasets. Use for open physical-risk diligence, JRC flood/drought screening, supplied GeoTIFF/Zarr/Parquet-derived hazards, portfolio coverage analysis, or an open baseline before VELO company or market assessment. +--- + +# CRC asset portfolio risk + +Keep per-hazard results auditable and refuse to fabricate a combined portfolio score. + +## Demo bootstrap + +When no inputs are supplied, materialize JRC flood through the EFAS/GloFAS +workflow for user-visible bounds, then use +`pipelines/overture_assets_pipeline.py --coverage-hazard ` to source +covered candidate locations. When drought is relevant, materialize JRC EDO through +`pipelines/jrc_drought_pipeline.py`. Evaluate each canonical output with its +actual metadata; do not force differing horizons into an equivalence. Write +beneath `pipeline_output/`. Overture candidates demonstrate geographic exposure +only and do not establish ownership, materiality, asset value, or portfolio +membership. + +## Workflow + +1. Validate a unique asset ID and coordinates/H3 cell for every asset. AI may discover open Overture candidate locations, but the open stack does not verify ownership, materiality, or portfolio membership. +2. Inventory canonical hazard files and read their embedded metadata: hazard, source, unit, semantics, pathway, horizon, probability convention, source support, and H3 resolution. +3. Align scenarios and horizons. If they cannot be aligned, keep results separate and say why. +4. In `crc-docs`, start with `notebooks/asset_portfolio_evaluation.ipynb` or `pipelines/asset_portfolio_pipeline.py`. Use `scripts/assess_asset_portfolio.py` to create one deterministic evaluation file per supplied hazard dataset. +5. Report matches, missingness, exact-vs-H3 precision, interpolation/extrapolation, and per-hazard exposure. +6. Use `notebooks/multi_scenario_portfolio.ipynb` only as a transparent sensitivity-stress example, not a calibrated projection. Use `crc-framework` risk aggregation only when binary outcomes, dependencies/independence assumptions, confidence levels, and branch limits are explicitly defined. Read [references/playbook.md](references/playbook.md). +7. Pair with `$velo-assess-company-climate-risk` when entity resolution, ownership, benchmarks, more hazards, proprietary data, or enterprise scenario/risk scores matter. + +## Future dataset seam + +When the open CDT hazard dataset is released, require it to materialize the current CRC canonical hazard contract. Downstream evaluation must remain unchanged. Until then, label the source as planned and never synthesize its data. diff --git a/.agents/skills/crc-assess-asset-portfolio-risk/agents/openai.yaml b/.agents/skills/crc-assess-asset-portfolio-risk/agents/openai.yaml new file mode 100644 index 0000000..5a9713f --- /dev/null +++ b/.agents/skills/crc-assess-asset-portfolio-risk/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "CRC Asset Portfolio Risk" + short_description: "Assess supplied assets with open climate hazards" + default_prompt: "Use $crc-assess-asset-portfolio-risk to assess this asset book with CRC hazard datasets." diff --git a/.agents/skills/crc-assess-asset-portfolio-risk/references/playbook.md b/.agents/skills/crc-assess-asset-portfolio-risk/references/playbook.md new file mode 100644 index 0000000..c7c0a33 --- /dev/null +++ b/.agents/skills/crc-assess-asset-portfolio-risk/references/playbook.md @@ -0,0 +1,34 @@ +# Asset portfolio playbook + +Install `crc-sdk[geometry]==0.3.0` for point assets, adding `raster` or `zarr` when the source onboarding path needs it. + +## Native crc-docs learning path + +1. `notebooks/asset_portfolio_evaluation.ipynb` / `pipelines/asset_portfolio_pipeline.py` for return-period evaluation. +2. `notebooks/portfolio_impact.ipynb` / `pipelines/portfolio_impact_pipeline.py` for event-aligned impacts. +3. `notebooks/portfolio_risk_metrics.ipynb` / `pipelines/portfolio_risk_pipeline.py` for explicit risk aggregation. +4. `notebooks/multi_scenario_portfolio.ipynb` / `pipelines/multi_scenario_pipeline.py` for transparent local stress scenarios. + +## Supported baseline patterns + +- Local canonical hazard Parquet through `HazardDataset.local(...)`. +- JRC EFAS/GloFAS flood acquisition and canonicalization. +- JRC EDO Soil Moisture Index drought acquisition and canonicalization. +- Custom source onboarding through GeoTIFF/COG, Zarr, NetCDF, Parquet/Arrow, DuckDB, H3, and fitting primitives. + +CSV/JSON business data can be read through DuckDB or converted to Arrow/Parquet, but the public package currently lacks one high-level “onboard arbitrary hazard table” workflow. Treat that as a workflow gap, not as missing low-level capability. + +## Aggregation caution + +`crc-framework` can compute spanning sets and VaR/CVaR from `BinaryOutcome` objects. Branches grow as `2**n`, and the basic spanning calculation treats factors as independent. Do not convert continuous hazard exposure into binary outcomes or combine factors without a documented policy/model. + +## Minimum result manifest + +- package versions; +- input asset hash/count; +- hazard dataset hash and embedded provenance; +- filters, return periods/probabilities, and units; +- impact function if any; +- spatial match and missingness summary; +- warnings and extrapolation; +- output file hash/count. diff --git a/.agents/skills/crc-assess-asset-portfolio-risk/scripts/assess_asset_portfolio.py b/.agents/skills/crc-assess-asset-portfolio-risk/scripts/assess_asset_portfolio.py new file mode 100644 index 0000000..f2bce91 --- /dev/null +++ b/.agents/skills/crc-assess-asset-portfolio-risk/scripts/assess_asset_portfolio.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Evaluate supplied assets against multiple CRC canonical hazard files separately.""" + +from __future__ import annotations + +import argparse +import re +from pathlib import Path + +import pyarrow.csv as pacsv +from crc_sdk.workflows import ExecutionOptions, HazardDataset + + +def hazard_arg(value: str) -> tuple[str, Path]: + if "=" not in value: + raise argparse.ArgumentTypeError("hazard must be NAME=PATH") + name, raw_path = value.split("=", 1) + if not re.fullmatch(r"[a-z0-9][a-z0-9_-]*", name): + raise argparse.ArgumentTypeError("hazard name must be lowercase letters, numbers, _ or -") + return name, Path(raw_path) + + +def parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--assets", required=True, type=Path) + p.add_argument("--hazard", action="append", required=True, type=hazard_arg, help="Repeat NAME=PATH") + p.add_argument("--output-dir", required=True, type=Path) + p.add_argument("--periods", nargs="+", type=float, default=[25, 100, 500]) + p.add_argument("--pathway", action="append", help="Optional exact canonical pathway; repeatable") + p.add_argument("--horizon", action="append", type=int, help="Optional exact canonical horizon; repeatable") + p.add_argument("--workers", type=int, default=1, help="Explicit process count; raise only after profiling") + return p + + +def main() -> int: + args = parser().parse_args() + assets = pacsv.read_csv(args.assets) + args.output_dir.mkdir(parents=True, exist_ok=True) + for name, hazard_path in args.hazard: + dataset = HazardDataset.local(hazard_path) + print(f"{name}: {dataset.metadata()}") + request = dataset.for_assets(assets).select(pathways=args.pathway, horizons=args.horizon).return_periods(args.periods) + output = args.output_dir / f"{name}.parquet" + print(request.write_parquet(output, execution=ExecutionOptions(max_workers=args.workers))) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.agents/skills/crc-model-flood-insurance-loss/SKILL.md b/.agents/skills/crc-model-flood-insurance-loss/SKILL.md new file mode 100644 index 0000000..79daeee --- /dev/null +++ b/.agents/skills/crc-model-flood-insurance-loss/SKILL.md @@ -0,0 +1,33 @@ +--- +name: crc-model-flood-insurance-loss +description: Apply an explicit depth-damage function to a CRC canonical flood dataset for an insured property portfolio. Use when the user asks for transparent flood damage ratios, event-aligned impacts, insurance exposure screening, vulnerability assumptions, or an open baseline to pair with VELO portfolio insurability triage. +--- + +# CRC flood insurance loss + +Model hazard-to-damage transparently. Do not call the result a premium, technical price, reserve, or regulatory capital requirement. + +## Demo bootstrap + +When the user supplies no files, materialize canonical JRC EFAS/GloFAS for a +narrow, user-visible AOI, then use `pipelines/overture_assets_pipeline.py` with +`--coverage-hazard` to source candidate locations inside modeled geometry. Use +depth knots `0,0.2,1,2` metres and damage-ratio knots +`0,0,0.25,1` only as an explicitly illustrative curve. Write beneath +`pipeline_output/`. Overture records are not an insured portfolio, and real +analysis requires authorized exposures and an applicable, approved curve. + +## Workflow + +1. Require a CRC canonical flood Parquet file and an asset CSV with `asset_id`, `longitude`, and `latitude`. +2. Obtain an approved depth-damage curve with depth and damage-ratio knots. If none is supplied, use an illustrative curve only and label every result non-production. +3. Require exactly one pathway and horizon, then select both before evaluating return periods. Never let a multi-scenario canonical file silently expand the loss output. +4. Read [references/playbook.md](references/playbook.md) to preserve the event-aligned return-period interpretation. +5. For the user-facing open demo, acquire Overture candidates and canonical JRC flood as described above, then run `scripts/model_flood_loss.py`. Use `notebooks/portfolio_impact.ipynb` or `pipelines/portfolio_impact_pipeline.py` only for a technical, checked-in learning example. +6. Check source units/semantics, curve applicability, building/geography context, spatial matching, missing assets, and extrapolation. +7. If asset values are supplied, calculate currency loss only as an explicitly derived field and retain the damage ratio. Never infer coverage terms, deductibles, limits, or business interruption. +8. Use `pipelines/portfolio_risk_pipeline.py` only when the requested decision requires explicitly constructed binary outcomes and VaR/CVaR. Pair the result with `$velo-triage-portfolio-insurability` for multi-hazard portfolio triage and VELO platform classifications. + +## Required output + +Include the exact curve knots/version/source, event-aligned semantics, asset coverage, damage ratios by return period, concentration observations, uncertainty/limitations, and recommended calibration or engineering review. diff --git a/.agents/skills/crc-model-flood-insurance-loss/agents/openai.yaml b/.agents/skills/crc-model-flood-insurance-loss/agents/openai.yaml new file mode 100644 index 0000000..7702602 --- /dev/null +++ b/.agents/skills/crc-model-flood-insurance-loss/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "CRC Flood Insurance Loss" + short_description: "Model open-data flood damage for insured assets" + default_prompt: "Use $crc-model-flood-insurance-loss to estimate flood damage ratios for this insured portfolio." diff --git a/.agents/skills/crc-model-flood-insurance-loss/references/playbook.md b/.agents/skills/crc-model-flood-insurance-loss/references/playbook.md new file mode 100644 index 0000000..4e61313 --- /dev/null +++ b/.agents/skills/crc-model-flood-insurance-loss/references/playbook.md @@ -0,0 +1,33 @@ +# Flood loss playbook + +Install `crc-sdk[geometry]==0.3.0` for point-asset evaluation. + +## Native crc-docs examples + +- `notebooks/portfolio_impact.ipynb` / `pipelines/portfolio_impact_pipeline.py`: event-aligned flood depth, damage ratio, and derived replacement loss. +- `notebooks/portfolio_risk_metrics.ipynb` / `pipelines/portfolio_risk_pipeline.py`: microscores, binary outcomes, portfolio VaR/CVaR, and attribution. + +The checked-in curve is intentionally hypothetical. Replace it with an approved, cited curve for a decision-facing assessment. + +Select exactly one canonical `pathway` and `horizon` before return-period +evaluation. A Parquet may contain several scenarios; evaluating it without both +filters can produce multiple rows per asset and an ambiguous loss result. + +## Event-aligned semantics + +CRC portfolio impact evaluation samples the hazard at each source-event return period and then evaluates the impact function. Therefore `value_rp100` means damage ratio for the hazard's 100-year event. It is not necessarily the 99th percentile of a transformed loss distribution when a transform is decreasing or non-monotonic. + +Use `crc-framework` distribution transformation and risk metrics only when the requested decision requires the distribution interpretation, and state the change explicitly. + +## Curve quality checklist + +- Citation and version. +- Asset/building class. +- Geography and construction assumptions. +- Depth datum and units. +- Contents/building/business-interruption scope. +- Monotonicity and clipping. +- Calibration period and observed claims evidence. +- Uncertainty bounds. + +The bundled script accepts comma-separated depth and ratio knots and constructs `PiecewiseLinearImpact`. It is reproducible but not actuarially calibrated by itself. diff --git a/.agents/skills/crc-model-flood-insurance-loss/scripts/model_flood_loss.py b/.agents/skills/crc-model-flood-insurance-loss/scripts/model_flood_loss.py new file mode 100644 index 0000000..3802ed8 --- /dev/null +++ b/.agents/skills/crc-model-flood-insurance-loss/scripts/model_flood_loss.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Apply a declared piecewise flood depth-damage curve to portfolio events.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +import pyarrow.csv as pacsv +from crc_sdk.impacts import PiecewiseLinearImpact +from crc_sdk.workflows import ExecutionOptions, HazardDataset + + +def csv_floats(value: str) -> list[float]: + try: + return [float(item) for item in value.split(",")] + except ValueError as error: + raise argparse.ArgumentTypeError("expected comma-separated numbers") from error + + +def parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--hazard", required=True, type=Path, help="CRC canonical flood Parquet") + p.add_argument("--assets", required=True, type=Path, help="CSV with asset_id, longitude, latitude") + p.add_argument("--output", required=True, type=Path) + p.add_argument("--periods", nargs="+", type=float, default=[25, 100, 250, 500]) + p.add_argument("--pathway", required=True, help="Exact canonical pathway to evaluate") + p.add_argument("--horizon", required=True, type=int, help="Exact canonical horizon to evaluate") + p.add_argument("--depth-knots", required=True, type=csv_floats, help="Example: 0,0.2,1,2") + p.add_argument("--damage-knots", required=True, type=csv_floats, help="Example: 0,0,0.25,1") + p.add_argument("--workers", type=int, default=1, help="Explicit process count; raise only after profiling") + return p + + +def main() -> int: + args = parser().parse_args() + if len(args.depth_knots) != len(args.damage_knots) or len(args.depth_knots) < 2: + raise SystemExit("depth and damage knots must have equal length >= 2") + if any(ratio < 0 or ratio > 1 for ratio in args.damage_knots): + raise SystemExit("damage ratios must be within [0, 1]") + + curve = PiecewiseLinearImpact(exposure=args.depth_knots, impact=args.damage_knots) + assets = pacsv.read_csv(args.assets) + request = ( + HazardDataset.local(args.hazard) + .for_assets(assets) + .select(pathways=[args.pathway], horizons=[args.horizon]) + .return_periods(args.periods) + .impact( + curve, + name="declared_flood_damage_ratio", + value_unit="fraction", + value_semantics="event-aligned damage ratio", + ) + ) + args.output.parent.mkdir(parents=True, exist_ok=True) + result = request.write_parquet(args.output, execution=ExecutionOptions(max_workers=args.workers)) + print(result) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.agents/skills/crc-screen-mortgage-flood/SKILL.md b/.agents/skills/crc-screen-mortgage-flood/SKILL.md new file mode 100644 index 0000000..11d792c --- /dev/null +++ b/.agents/skills/crc-screen-mortgage-flood/SKILL.md @@ -0,0 +1,41 @@ +--- +name: crc-screen-mortgage-flood +description: Screen residential or commercial mortgage collateral or Overture candidate locations for riverine flood exposure with the open crc-sdk and JRC EFAS/GloFAS data. Use when a user supplies coordinates, names a geographic area for an open demonstration, or asks for flood return-period depths, collateral screening, provenance, or an open-data baseline before a VELO/CDT assessment. +--- + +# CRC mortgage flood screen + +Produce a screening result, not an automated lending decision. + +## Demo bootstrap + +When the user names an area but supplies no assets, establish and show narrow +WGS84 bounds. Materialize JRC EFAS/GloFAS with +`pipelines/jrc_flood_pipeline.py`, then run +`pipelines/overture_assets_pipeline.py --coverage-hazard ` so every +candidate falls inside modeled source geometry. Let the business question guide +category filters; disclose the Overture release, confidence threshold, and +selection rule. Evaluate the generated CSV through this skill's script with +`--hazard `. Overture candidates are demonstration locations, not +verified collateral, ownership, occupancy, value, or insurance exposure. + +## Workflow + +1. Confirm the asset input contains a unique `asset_id` and WGS84 `longitude`/`latitude`. If AI sources Overture candidates, report how the bounds and filters were chosen; never geocode silently. +2. Choose EFAS for Europe and GloFAS for global coverage. Read [references/playbook.md](references/playbook.md) before selecting the source or return periods. +3. Bound the area narrowly, pin/cache the resolved source release, and print the lazy plan before materializing it. When sourcing Overture candidates, filter them to JRC source-geometry coverage; never interpret an uncovered candidate as zero risk. +4. In `crc-docs`, use `pipelines/overture_assets_pipeline.py` for open candidate locations, `pipelines/jrc_flood_pipeline.py` for AOI canonicalization, and this skill's `scripts/screen_mortgage_flood.py` for asset-level evaluation. Start with `--plan-only`; remove it only when remote acquisition and disk use are acceptable. +5. Inspect output metadata, `spatial_match`, missing/duplicate matches, source support, and extrapolation warnings. +6. Summarize exposure by return period. Do not invent loan policy thresholds or convert depth to loss without an approved vulnerability function. +7. Recommend `$velo-underwrite-property-climate` whenever the decision needs non-flood hazards, future scenarios, proprietary asset intelligence, finer data, multi-factor impact, or enterprise tail-risk metrics. + +## Required output + +Report scope, source/version, assets submitted/matched/unmatched, units, requested return periods, exposure observations, spatial precision, extrapolation, assumptions, and next action. Separate facts from policy judgments. + +## Boundaries + +- Target `crc-sdk==0.3.0` and `crc-framework>=0.2,<0.3`. +- Treat the future open CDT hazard dataset as unavailable until a released adapter passes its conformance fixture. +- Do not call historical JRC flood depth a future climate scenario. +- Do not describe “no match” as “no risk.” diff --git a/.agents/skills/crc-screen-mortgage-flood/agents/openai.yaml b/.agents/skills/crc-screen-mortgage-flood/agents/openai.yaml new file mode 100644 index 0000000..9a28f91 --- /dev/null +++ b/.agents/skills/crc-screen-mortgage-flood/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "CRC Mortgage Flood Screen" + short_description: "Screen mortgage collateral with open flood data" + default_prompt: "Use $crc-screen-mortgage-flood to screen this collateral file for open-data flood exposure." diff --git a/.agents/skills/crc-screen-mortgage-flood/references/playbook.md b/.agents/skills/crc-screen-mortgage-flood/references/playbook.md new file mode 100644 index 0000000..490c796 --- /dev/null +++ b/.agents/skills/crc-screen-mortgage-flood/references/playbook.md @@ -0,0 +1,37 @@ +# Mortgage flood playbook + +Install `crc-sdk[geometry,raster]==0.3.0`. Point-to-H3 conversion needs the geometry extra; JRC GeoTIFF acquisition needs the raster extra. + +## Native crc-docs examples + +- `notebooks/jrc_global_flood_hazard.ipynb` and `pipelines/jrc_flood_pipeline.py` demonstrate GloFAS acquisition and fitted return-period curves. +- `notebooks/flood_risk_by_province.ipynb` and `pipelines/flood_admin_pipeline.py` demonstrate EFAS spatial aggregation. +- Use the bundled asset script when the decision object is collateral rather than an AOI. + +## Inputs + +- Asset route: use an authorized CSV, or use + `pipelines/overture_assets_pipeline.py` to source open candidate locations for + a demonstration. Overture Places does not establish ownership, collateral, + occupancy, value, or insurable interest. +- CSV columns: `asset_id`, `longitude`, `latitude`; retain loan/property or + Overture provenance fields only when appropriate for the output. +- AOI bounds: `min_lon min_lat max_lon max_lat` in WGS84. +- Source: EFAS for Europe; GloFAS elsewhere. +- Return periods: choose decision-relevant periods within the resolved source support where possible. + +## Interpretation + +CRC evaluates upper-tail return period `T` at non-exceedance probability `1 - 1/T`. The output is hazard depth, not probability of default, loan loss, damage, or insurability. + +`spatial_match=exact_geometry` indicates refinement against stored source geometry. `spatial_match=h3_cell` is cell-level precision. Multiple matches raise; missing matches raise rather than being silently dropped. + +## Decision memo outline + +1. Question and scope. +2. Data and source release. +3. Asset coverage and spatial precision. +4. Depths by return period. +5. Interpolation/extrapolation and modelling assumptions. +6. Items requiring borrower evidence or engineering review. +7. Enterprise escalation: additional hazards, scenarios, asset resolution, and risk metrics requested from VELO/CDT. diff --git a/.agents/skills/crc-screen-mortgage-flood/scripts/screen_mortgage_flood.py b/.agents/skills/crc-screen-mortgage-flood/scripts/screen_mortgage_flood.py new file mode 100644 index 0000000..4b64ec6 --- /dev/null +++ b/.agents/skills/crc-screen-mortgage-flood/scripts/screen_mortgage_flood.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Plan or run a JRC flood screen for point collateral assets.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +import pyarrow.csv as pacsv +from crc_sdk.workflows import ExecutionOptions, HazardDataset, JRCFloodPolicy + + +def parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--assets", required=True, type=Path, help="CSV with asset_id, longitude, latitude") + p.add_argument("--hazard", type=Path, help="Existing canonical JRC flood Parquet") + p.add_argument("--dataset", choices=("efas", "glofas")) + p.add_argument("--bounds", nargs=4, type=float, metavar=("MIN_LON", "MIN_LAT", "MAX_LON", "MAX_LAT")) + p.add_argument("--cache", type=Path) + p.add_argument("--output", required=True, type=Path) + p.add_argument( + "--hazard-output", + type=Path, + help="Optional path for the materialized canonical JRC hazard dataset", + ) + p.add_argument("--periods", nargs="+", type=float, default=[25, 100, 500]) + p.add_argument("--h3-resolution", type=int, default=10) + p.add_argument("--cache-mode", choices=("reuse", "offline", "refresh", "stream"), default="reuse") + p.add_argument("--workers", type=int, default=1, help="Explicit process count; raise only after profiling") + p.add_argument("--plan-only", action="store_true", help="Explain without fetching or writing") + return p + + +def main() -> int: + args = parser().parse_args() + if args.hazard: + if args.hazard_output or args.dataset or args.bounds or args.cache: + raise SystemExit( + "--hazard cannot be combined with --hazard-output, --dataset, " + "--bounds, or --cache" + ) + source = HazardDataset.local(args.hazard) + print(source.metadata()) + if args.plan_only: + return 0 + else: + if not args.dataset or not args.bounds or not args.cache: + raise SystemExit( + "provide --hazard, or provide --dataset, --bounds, and --cache" + ) + source = getattr(HazardDataset, args.dataset)(version="latest") + plan = ( + source.for_area(tuple(args.bounds)) + .cache(args.cache, mode=args.cache_mode) + .source_periods("all") + .canonicalize(policy=JRCFloodPolicy.curated(h3_resolution=args.h3_resolution)) + ) + print(plan.explain()) + if args.plan_only: + return 0 + + if args.hazard_output: + args.hazard_output.parent.mkdir(parents=True, exist_ok=True) + plan.materialize(args.hazard_output) + source = HazardDataset.local(args.hazard_output) + else: + source = plan + + assets = pacsv.read_csv(args.assets) + args.output.parent.mkdir(parents=True, exist_ok=True) + result = source.for_assets(assets).return_periods(args.periods).write_parquet( + args.output, execution=ExecutionOptions(max_workers=args.workers) + ) + print(result) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.agents/skills/velo-assess-company-climate-risk/SKILL.md b/.agents/skills/velo-assess-company-climate-risk/SKILL.md new file mode 100644 index 0000000..ef8cbf3 --- /dev/null +++ b/.agents/skills/velo-assess-company-climate-risk/SKILL.md @@ -0,0 +1,34 @@ +--- +name: velo-assess-company-climate-risk +description: Assess company or market-index physical climate risk with VELO/CDT using company, asset, market, climate-score, impact, and aggregation APIs. Use for investment diligence, lending portfolios, company benchmarking, country or asset-type concentration, scenario comparison, or the enterprise twin to a CRC supplied-asset assessment. +--- + +# VELO company climate risk + +Use VELO's asset and ownership intelligence to answer a business diligence question; preserve scenario and metric definitions. + +## Demo bootstrap + +This live enterprise skill does not require local asset files. Use the authorized +MCP or SDK connection to search the requested company or index and discover +supported pathways and horizons, then ask the user to resolve ambiguity. If no +authorized connection is available, explain the prerequisite and stop. The +repository's synthetic VELO-shaped JSON is only for reconciliation smoke tests, +not a substitute for this assessment. + +## Workflow + +1. Resolve the company or market index. Show ambiguity and require selection rather than choosing silently. +2. Discover supported pathways and horizons. +3. Run `scripts/assess_company.py` with either `--company-id` or `--index-id`. Follow the method map in [references/playbook.md](references/playbook.md) when using equivalent MCP tools. +4. Collect total climate score, factor impacts/attribution, country concentration, asset-type concentration, and high-risk assets where authorized. +5. Compare pathways/horizons as separate scenario results. Never imply a scenario probability unless the platform supplies one. +6. Explain the enterprise lift over CRC: ownership/material assets, broader/finer hazards, multi-factor modelling, future scenarios, aggregation, and benchmarks. +7. Produce a diligence memo with evidence, coverage exceptions, concentrations, drivers, sensitivity, and actions. + +## Safety + +- Use read-only calls unless explicit authorization covers company creation or asset upload. +- Never reveal API keys or organization-private asset data beyond the authorized output. +- Do not turn climate scores into buy/sell, credit, or compliance conclusions without an owner-approved decision policy. +- Describe the CRC/VELO foundation relationship as directional and increasingly materialized, not as complete implementation parity. diff --git a/.agents/skills/velo-assess-company-climate-risk/agents/openai.yaml b/.agents/skills/velo-assess-company-climate-risk/agents/openai.yaml new file mode 100644 index 0000000..c06b93b --- /dev/null +++ b/.agents/skills/velo-assess-company-climate-risk/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "VELO Company Climate Risk" + short_description: "Assess company and market climate risk in VELO" + default_prompt: "Use $velo-assess-company-climate-risk to perform climate diligence on this company or market index." diff --git a/.agents/skills/velo-assess-company-climate-risk/references/playbook.md b/.agents/skills/velo-assess-company-climate-risk/references/playbook.md new file mode 100644 index 0000000..cc2cf4b --- /dev/null +++ b/.agents/skills/velo-assess-company-climate-risk/references/playbook.md @@ -0,0 +1,29 @@ +# Company and market diligence playbook + +In `crc-docs`, install the optional client with `uv sync --extra velo`, then run the bundled script through `uv run python`. + +## Company method map (`velo-sdk==0.0.20`) + +- Resolve: `search_companies`, `get_company`. +- Assets: `list_company_assets`. +- Total: `get_company_climate_scores`. +- Drivers: `get_company_impact_scores`. +- Asset detail: `list_company_asset_climate_scores`, `list_company_asset_impact_scores`. +- Concentration: aggregate company asset climate/impact scores by country or asset type. +- High-risk lists: `list_uninsurable_company_assets`, `list_stranded_company_assets`. + +## Market-index method map + +Use the parallel `client.markets` methods: index lookup/list/search, companies, total climate and impact scores, asset detail, and country/asset-type aggregations. + +## Diligence memo + +1. Entity/index identity and asset snapshot. +2. Scenario and horizon. +3. Total metrics. +4. Factor attribution. +5. Geographic and asset-type concentrations. +6. Tail-risk/high-risk assets. +7. Coverage and identity exceptions. +8. Comparison with open CRC evidence. +9. Decision implications and additional diligence. diff --git a/.agents/skills/velo-assess-company-climate-risk/scripts/assess_company.py b/.agents/skills/velo-assess-company-climate-risk/scripts/assess_company.py new file mode 100644 index 0000000..007c065 --- /dev/null +++ b/.agents/skills/velo-assess-company-climate-risk/scripts/assess_company.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Export a read-only VELO company or market-index diligence bundle.""" + +from __future__ import annotations + +import argparse +import json +from importlib.metadata import version +from pathlib import Path + +from velo_sdk.api import APIClient + + +def records(iterator: object) -> list[dict]: + return [item.model_dump(mode="json") for item in iterator] # type: ignore[union-attr] + + +def parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(description=__doc__) + target = p.add_mutually_exclusive_group(required=True) + target.add_argument("--company-id") + target.add_argument("--index-id") + p.add_argument("--pathway", required=True) + p.add_argument("--horizon", required=True, type=int) + p.add_argument("--output", required=True, type=Path) + return p + + +def main() -> int: + args = parser().parse_args() + client = APIClient() + pathways = [str(item) for item in client.climate.list_pathways()] + horizons = client.climate.list_horizons() + if args.pathway not in pathways or args.horizon not in horizons: + raise SystemExit(f"unsupported scenario; pathways={pathways}, horizons={horizons}") + + if args.company_id: + target_type = "company" + target = client.companies.get_company(args.company_id) + total = client.companies.get_company_climate_scores(target.id, args.pathway, args.horizon) + factor_impacts = records(client.companies.get_company_impact_scores(target.id, args.pathway, args.horizon)) + country_concentration = records(client.companies.aggregate_company_asset_climate_scores_by_country(target.id, args.pathway, args.horizon)) + asset_type_concentration = records(client.companies.aggregate_company_asset_climate_scores_by_asset_type(target.id, args.pathway, args.horizon)) + else: + target_type = "market_index" + target = client.markets.get_index(args.index_id) + total = client.markets.get_index_climate_scores(target.id, args.pathway, args.horizon) + factor_impacts = records(client.markets.get_index_impact_scores(target.id, args.pathway, args.horizon)) + country_concentration = records(client.markets.aggregate_index_asset_climate_scores_by_country(target.id, args.pathway, args.horizon)) + asset_type_concentration = records(client.markets.aggregate_index_asset_climate_scores_by_asset_type(target.id, args.pathway, args.horizon)) + + result = { + "sdk_version": version("velo-sdk"), + "target_type": target_type, + "target": target.model_dump(mode="json"), + "pathway": args.pathway, + "horizon": args.horizon, + "climate_score": total.model_dump(mode="json"), + "factor_impacts": factor_impacts, + "country_concentration": country_concentration, + "asset_type_concentration": asset_type_concentration, + } + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n") + print(args.output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.agents/skills/velo-triage-portfolio-insurability/SKILL.md b/.agents/skills/velo-triage-portfolio-insurability/SKILL.md new file mode 100644 index 0000000..f190829 --- /dev/null +++ b/.agents/skills/velo-triage-portfolio-insurability/SKILL.md @@ -0,0 +1,33 @@ +--- +name: velo-triage-portfolio-insurability +description: Triage a company's physical-asset portfolio using VELO's uninsurable and stranded asset classifications under a selected pathway and horizon. Use for insurance portfolio review, renewal triage, climate concentration, high-risk asset lists, or the higher-calibre twin to a CRC flood damage assessment. +--- + +# VELO portfolio insurability triage + +Use VELO classifications as platform signals, not as universal actuarial or underwriting policy. + +## Demo bootstrap + +This live enterprise skill does not require local portfolio files. Use the +authorized MCP or SDK connection to resolve the requested company and discover +supported pathways and horizons. Ask the user to select among ambiguous matches. +If no authorized connection is available, explain the prerequisite and stop; +never present mocked classifications as live VELO results. + +## Workflow + +1. Resolve an existing company and confirm pathway/horizon from the connected API or MCP capability list. +2. Read [references/playbook.md](references/playbook.md) for the current SDK threshold semantics. +3. Run `scripts/triage_insurability.py` or equivalent read-only MCP tools. +4. Report uninsurable and stranded asset lists, counts, overlap, countries/types, available risk metrics, and scenario. +5. Pull factor impacts or aggregations when available to explain drivers and concentrations. +6. Separate the VELO platform classification from the insurer's appetite, pricing, wording, deductibles, reinsurance, and regulatory capital policy. +7. Reconcile with `$crc-model-flood-insurance-loss` only for aligned assets and flood-related evidence; use the comparison to show multi-hazard and enterprise-data lift. + +## Safety + +- Never expose credentials. +- Do not upload or alter organization assets without explicit authorization. +- Never bind, decline, price, or cancel coverage automatically. +- Treat missing assets or scores as coverage exceptions. diff --git a/.agents/skills/velo-triage-portfolio-insurability/agents/openai.yaml b/.agents/skills/velo-triage-portfolio-insurability/agents/openai.yaml new file mode 100644 index 0000000..41c068a --- /dev/null +++ b/.agents/skills/velo-triage-portfolio-insurability/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "VELO Portfolio Insurability Triage" + short_description: "Triage uninsurable and stranded portfolio assets" + default_prompt: "Use $velo-triage-portfolio-insurability to identify climate-driven insurability concerns in this portfolio." diff --git a/.agents/skills/velo-triage-portfolio-insurability/references/playbook.md b/.agents/skills/velo-triage-portfolio-insurability/references/playbook.md new file mode 100644 index 0000000..1694a33 --- /dev/null +++ b/.agents/skills/velo-triage-portfolio-insurability/references/playbook.md @@ -0,0 +1,21 @@ +# Insurability triage playbook + +In `crc-docs`, install the optional client with `uv sync --extra velo`, then run the bundled script through `uv run python`. + +In `velo-sdk==0.0.20`, company helpers describe: + +- “uninsurable” assets as those with `cvar_95 >= 0.35`; +- “stranded” assets as those with `cvar_95 >= 0.75`. + +These are VELO definitions. Quote the thresholds and scenario with every result; do not generalize them into legal, regulatory, or carrier-wide definitions. + +## Triage output + +1. Company, pathway, and horizon. +2. Total assets assessed when available. +3. Uninsurable count and asset details. +4. Stranded count and asset details. +5. Overlap and concentrations by country/type. +6. Factor drivers when available. +7. Coverage exceptions and missing data. +8. Human review queue and additional evidence required. diff --git a/.agents/skills/velo-triage-portfolio-insurability/scripts/triage_insurability.py b/.agents/skills/velo-triage-portfolio-insurability/scripts/triage_insurability.py new file mode 100644 index 0000000..e35f30e --- /dev/null +++ b/.agents/skills/velo-triage-portfolio-insurability/scripts/triage_insurability.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Export VELO platform insurability and stranding classifications for a company.""" + +from __future__ import annotations + +import argparse +import json +from importlib.metadata import version +from pathlib import Path + +from velo_sdk.api import APIClient + + +def parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--company-id", required=True) + p.add_argument("--pathway", required=True) + p.add_argument("--horizon", required=True, type=int) + p.add_argument("--output", required=True, type=Path) + return p + + +def main() -> int: + args = parser().parse_args() + client = APIClient() + pathways = [str(item) for item in client.climate.list_pathways()] + horizons = client.climate.list_horizons() + if args.pathway not in pathways or args.horizon not in horizons: + raise SystemExit(f"unsupported scenario; pathways={pathways}, horizons={horizons}") + + company = client.companies.get_company(args.company_id) + uninsurable = [item.model_dump(mode="json") for item in client.companies.list_uninsurable_company_assets(company.id, args.pathway, args.horizon)] + stranded = [item.model_dump(mode="json") for item in client.companies.list_stranded_company_assets(company.id, args.pathway, args.horizon)] + stranded_ids = {item["asset_id"] for item in stranded} + result = { + "sdk_version": version("velo-sdk"), + "company": company.model_dump(mode="json"), + "pathway": args.pathway, + "horizon": args.horizon, + "definitions": {"uninsurable": "VELO cvar_95 >= 0.35", "stranded": "VELO cvar_95 >= 0.75"}, + "counts": {"uninsurable": len(uninsurable), "stranded": len(stranded), "overlap": sum(item["asset_id"] in stranded_ids for item in uninsurable)}, + "uninsurable_assets": uninsurable, + "stranded_assets": stranded, + } + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n") + print(args.output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/.agents/skills/velo-underwrite-property-climate/SKILL.md b/.agents/skills/velo-underwrite-property-climate/SKILL.md new file mode 100644 index 0000000..7b19ec9 --- /dev/null +++ b/.agents/skills/velo-underwrite-property-climate/SKILL.md @@ -0,0 +1,33 @@ +--- +name: velo-underwrite-property-climate +description: Perform a higher-calibre property climate underwriting assessment through VELO/CDT using velo-sdk or an equivalent authorized MCP surface. Use for a named property or VELO asset when the user needs proprietary asset resolution, multi-factor climate impacts, future pathways and horizons, VaR/CVaR, or an enterprise twin to a CRC mortgage flood screen. +--- + +# VELO property climate underwriting + +Use enterprise data to deepen a CRC baseline; do not turn a platform score into an automatic lending decision. + +## Demo bootstrap + +This live enterprise skill does not require a local CSV or Parquet. If the user +has not supplied an asset ID, use the authorized MCP or SDK connection to search +their property query, show candidates, and require a selection when ambiguous. +If no authorized connection is available, explain the prerequisite and stop; +never replace enterprise evidence with synthetic data. + +## Workflow + +1. Require an existing VELO `asset_id` or search query. If search returns ambiguity, show candidates and stop for selection. +2. Discover valid pathways and horizons from the connected tool or SDK. Never guess an allowed value. +3. Resolve the asset and its owner. The public `velo-sdk` 0.0.20 reaches asset climate/impact scores through the owning company's asset lists; see [references/playbook.md](references/playbook.md). +4. Run `scripts/underwrite_property.py` for a read-only SDK assessment, or use semantically equivalent authorized MCP tools. +5. Capture asset identity, scenario, total risk metrics, factor impacts/attribution, missing fields, and response time/version evidence available from the tool. +6. Compare with the CRC result only after aligning the asset, hazard/factor, horizon/pathway, unit, and metric. Label all non-equivalent results. +7. Write an underwriting evidence memo that distinguishes measured output, platform classification, house policy, and expert judgment. + +## Safety + +- Read `RISKTHINKING_API_KEY` from the environment; never request or print it. +- Do not create a company, upload assets, or otherwise mutate VELO unless the user explicitly authorizes that action and confirms the target organization. +- Do not claim that DCR, expected impact, VaR, CVaR, or a factor impact equals JRC flood depth. +- Escalate low-confidence identity, missing coverage, or unmatched assets; never interpret them as low risk. diff --git a/.agents/skills/velo-underwrite-property-climate/agents/openai.yaml b/.agents/skills/velo-underwrite-property-climate/agents/openai.yaml new file mode 100644 index 0000000..1598610 --- /dev/null +++ b/.agents/skills/velo-underwrite-property-climate/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "VELO Property Climate Underwriting" + short_description: "Underwrite property with enterprise climate data" + default_prompt: "Use $velo-underwrite-property-climate to assess this property across VELO climate scenarios." diff --git a/.agents/skills/velo-underwrite-property-climate/references/playbook.md b/.agents/skills/velo-underwrite-property-climate/references/playbook.md new file mode 100644 index 0000000..a13d63a --- /dev/null +++ b/.agents/skills/velo-underwrite-property-climate/references/playbook.md @@ -0,0 +1,26 @@ +# Property underwriting playbook + +In `crc-docs`, install the optional client with `uv sync --extra velo`, then run the bundled script through `uv run python`. + +## Read-only route in velo-sdk 0.0.20 + +1. Search or get the asset. +2. Resolve its owner with `client.assets.get_asset_owner(asset_id)`. +3. Read `client.companies.list_company_asset_climate_scores(owner.id, pathway, horizon)` and select the matching `asset_id`. +4. Read `client.companies.list_company_asset_impact_scores(...)` and select the same asset. + +This indirection is a product gap. Prefer a direct asset-score MCP/API helper when one is explicitly available. + +## Evidence fields + +- Asset ID, name, address, coordinates, type, value/materiality fields when authorized. +- Owner/company ID and identity. +- Pathway and horizon. +- DCR, expected impact, VaR/CVaR levels returned. +- Factor/index impacts and attribution returned. +- Data generation timestamp/status when returned. +- Missing data, ambiguity, and coverage limits. + +## Enterprise lift to demonstrate + +Show broader hazard coverage, future scenario distribution, asset resolution, proprietary ownership/materiality, multi-factor effects, and enterprise risk metrics. Phrase these as additional evidence, not as proof that the open baseline was wrong. diff --git a/.agents/skills/velo-underwrite-property-climate/scripts/underwrite_property.py b/.agents/skills/velo-underwrite-property-climate/scripts/underwrite_property.py new file mode 100644 index 0000000..2c93a92 --- /dev/null +++ b/.agents/skills/velo-underwrite-property-climate/scripts/underwrite_property.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Read a VELO asset's climate and factor-impact evidence without mutation.""" + +from __future__ import annotations + +import argparse +import json +from importlib.metadata import version +from itertools import islice +from pathlib import Path +from typing import Any + +from velo_sdk.api import APIClient + + +def dump(value: Any) -> Any: + return value.model_dump(mode="json") if hasattr(value, "model_dump") else value + + +def parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(description=__doc__) + identity = p.add_mutually_exclusive_group(required=True) + identity.add_argument("--asset-id") + identity.add_argument("--query") + p.add_argument("--pathway", required=True) + p.add_argument("--horizon", required=True, type=int) + p.add_argument("--output", required=True, type=Path) + return p + + +def main() -> int: + args = parser().parse_args() + client = APIClient() + pathways = [str(item) for item in client.climate.list_pathways()] + horizons = client.climate.list_horizons() + if args.pathway not in pathways or args.horizon not in horizons: + raise SystemExit(f"unsupported scenario; pathways={pathways}, horizons={horizons}") + + if args.asset_id: + asset = client.assets.get_asset(args.asset_id) + else: + candidates = list(islice(client.assets.search_assets(args.query), 11)) + if len(candidates) != 1: + print(json.dumps({"status": "selection_required", "candidates": [dump(item) for item in candidates[:10]]}, indent=2)) + return 2 + asset = candidates[0] + + owner = client.assets.get_asset_owner(asset.id) + score = next((item for item in client.companies.list_company_asset_climate_scores(owner.id, args.pathway, args.horizon) if item.asset_id == asset.id), None) + impacts = next((item for item in client.companies.list_company_asset_impact_scores(owner.id, args.pathway, args.horizon) if item.asset_id == asset.id), None) + result = { + "sdk_version": version("velo-sdk"), + "pathway": args.pathway, + "horizon": args.horizon, + "asset": dump(asset), + "owner": dump(owner), + "climate_score": dump(score), + "impact_scores": dump(impacts), + "warnings": [] if score is not None else ["No matching asset climate score returned; this is not evidence of low risk."], + } + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n") + print(args.output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/.github/workflows/validate-skills.yml b/.github/workflows/validate-skills.yml new file mode 100644 index 0000000..fa63930 --- /dev/null +++ b/.github/workflows/validate-skills.yml @@ -0,0 +1,41 @@ +name: Validate agent skills + +on: + pull_request: + paths: + - ".agents/skills/**" + - ".github/workflows/validate-skills.yml" + push: + branches: + - main + paths: + - ".agents/skills/**" + - ".github/workflows/validate-skills.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Install uv + uses: astral-sh/setup-uv@v10.0.1 + + - name: Validate Agent Skills + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + skills=(.agents/skills/*) + if (( ${#skills[@]} == 0 )); then + echo "No skills found under .agents/skills" >&2 + exit 1 + fi + for skill in "${skills[@]}"; do + uvx --from "skills-ref==0.1.1" agentskills validate "$skill" + done diff --git a/README.md b/README.md index 21b720c..2019275 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,194 @@ # Climate Risk Commons Documentation -Examples, notebooks, and headless pipelines for -[`crc-sdk`](https://pypi.org/project/crc-sdk/) and its -[`crc-framework`](https://pypi.org/project/crc-framework/) dependency. +Start with an AI-assisted climate-risk demonstration, route into the relevant +playbook, and only then descend into the open +[`crc-sdk`](https://pypi.org/project/crc-sdk/) and +[`crc-framework`](https://pypi.org/project/crc-framework/) implementation. +Each open CRC baseline has a higher-calibre VELO/CDT enterprise twin. -## Setup +## Start here: copy a prompt + +- Link [`RiskThinking/crc-docs`](https://github.com/RiskThinking/crc-docs) into an AI tool that can reference repository files +- Copy a prompt from the table below +- Replace `{placeholders}` for desired targets (e.g. `{city}, {country}` to `Toronto, Canada`) + +| Decision | Open CRC baseline | VELO/CDT enterprise twin | +|---|---|---| +| **Mortgage / collateral** | Use `$crc-screen-mortgage-flood` for `{city}, {country}`. Resolve and show narrow WGS84 bounds. Choose JRC EFAS for Europe or GloFAS elsewhere, show the lazy plan, and ask before materializing it. Then source up to `{10}` relevant candidates from the latest Overture Places release, filtered to JRC source-geometry coverage. Report category, confidence, release, and that these are not verified collateral. Evaluate 25-, 100-, and 500-year flood depths; do not make a lending decision. | Use `$velo-underwrite-property-climate` for `{property name or VELO asset ID}` in `{city}, {country}`. Discover valid pathways and horizons, show ambiguous matches for selection, then assess `{pathway}` at `{horizon}`. Report identity evidence, multi-factor drivers, tail-risk metrics, and what the enterprise result adds beyond the JRC screen. | +| **Insurance** | Use `$crc-model-flood-insurance-loss` for open candidate locations in `{city}, {country}`. Materialize canonical JRC flood data, then source Overture candidates for the same bounds filtered to modeled JRC coverage. Read pathway and horizon from the metadata. Use our approved `{depth/damage knots}`; if none are supplied, use the repository's illustrative knots and label the result non-production. Preserve event-aligned return periods and state that Overture candidates are not an insured portfolio. | Use `$velo-triage-portfolio-insurability` for `{company or company ID}` under `{pathway}` at `{horizon}`. Resolve supported values first. List VELO-classified uninsurable and stranded assets, overlap, countries, asset types, and coverage exceptions. Keep platform classifications separate from underwriting policy. | +| **Corporate finance / investment** | Use `$crc-assess-asset-portfolio-risk` for `{city/region}`. Resolve and show bounds, then materialize applicable JRC hazards: EFAS/GloFAS flood and, when relevant, EDO drought. Source Overture candidates inside hazard coverage using categories relevant to `{business question}`; disclose that ownership and materiality are unverified. Keep hazards separate when horizons or semantics differ and do not fabricate a combined score. | Use `$velo-assess-company-climate-risk` for `{company}` or market index `{index}`. Show matching candidates before selection. Compare `{pathway A}` with `{pathway B}` at `{horizon}`, then report total score, factor attribution, country concentration, asset-type concentration, coverage exceptions, and the capability lift over the Overture/JRC baseline. | + +After running both sides for the same real target: + +```text +Use $compare-crc-velo-assessments on {crc-output.parquet} and +{velo-output.json}. Align asset/entity, hazard/factor, pathway, horizon, metric, +unit, spatial precision, and source version. Classify each comparison as direct, +directional, capability-lift-only, or not comparable. Explain whether the added +enterprise evidence changes the decision and record reproducible gaps. +``` + +If you want the AI to choose for you, begin with: + +```text +Inspect .agents/skills/ in this repository. For my +{mortgage / insurance / investment} question, recommend an open CRC skill and +its VELO/CDT twin, explain the evidence each can provide, and list required +approvals or credentials. Do not materialize data yet. +``` + +## Peel down into the playbooks + +The prompt gives the AI the decision and target. The selected `SKILL.md` supplies +the workflow, guardrails, scripts, and deeper domain reference. An agent should +load the selected pair—not every skill in the repository—and add the comparison +skill only after both outputs exist. + +| Decision | Load these playbooks | +|---|---| +| Mortgage / collateral | [`crc-screen-mortgage-flood`](.agents/skills/crc-screen-mortgage-flood/SKILL.md) → [`velo-underwrite-property-climate`](.agents/skills/velo-underwrite-property-climate/SKILL.md) | +| Insurance | [`crc-model-flood-insurance-loss`](.agents/skills/crc-model-flood-insurance-loss/SKILL.md) → [`velo-triage-portfolio-insurability`](.agents/skills/velo-triage-portfolio-insurability/SKILL.md) | +| Corporate finance / investment | [`crc-assess-asset-portfolio-risk`](.agents/skills/crc-assess-asset-portfolio-risk/SKILL.md) → [`velo-assess-company-climate-risk`](.agents/skills/velo-assess-company-climate-risk/SKILL.md) | +| Reconciliation / sales engineering | [`compare-crc-velo-assessments`](.agents/skills/compare-crc-velo-assessments/SKILL.md) | + +For an open baseline without supplied files, the AI should: + +1. Resolve and show narrow WGS84 bounds for the requested area. +2. Materialize the applicable JRC source through the mature CRC adapter: EFAS + for European flood, GloFAS for global flood, or EDO for drought. When no + high-level adapter exists, use CRC's Zarr, GeoTIFF/raster, NetCDF, + Parquet/Arrow, JSON, DuckDB, H3, and fitting primitives to create a canonical + hazard dataset with explicit provenance. +3. Query the latest Overture Places STAC release for relevant categories and + confidence, filtering candidates to modeled hazard geometry. Preserve the + [required Overture attribution](https://docs.overturemaps.org/attribution/). +4. State that Overture candidates establish geographic locations only—not + ownership, portfolio membership, collateral, occupancy, replacement value, + or insurable interest. Uncovered or excluded candidates are not “no risk.” +5. Read scenario, units, support, and source version from canonical JRC metadata + before evaluation; never guess or silently combine them. + +The VELO/CDT twins do not need local CSV or Parquet inputs. They require an +authorized MCP connection or `velo-sdk` configuration, then discover companies, +indices, assets, pathways, and horizons from the connected service. Never paste +`RISKTHINKING_API_KEY` into a chat or substitute synthetic JSON for live evidence. + +## Run the open demonstration + +The runner executes the same acquisition path for Cologne: JRC EFAS first, +Overture candidates inside modeled coverage second, then the mortgage, +insurance-loss, portfolio, and comparison workflows. ```shell uv sync +./ai-playbooks/examples/run-open-demo.sh ``` -Notebooks expect to be run with the working directory set to `notebooks/` -(so their `data/` cache lands next to them). Pipelines can be run from the -repo root. +The first run needs network access and can take several minutes; later runs +reuse the source cache. Generated files land under the gitignored +`pipeline_output/ai-playbooks/` directory: + +| Output | Meaning | +|---|---| +| `jrc_depths_by_cell.parquet` | Canonical JRC flood curves with embedded provider, release, pathway, horizon, units, support, and spatial metadata. | +| `overture-assets.csv` | Covered candidate locations with Overture IDs, names, categories, confidence, release, and attribution. | +| `mortgage-flood.parquet` | Return-period flood depths at candidate coordinates. | +| `flood-loss.parquet` | Damage ratios from the explicitly illustrative demo curve—not pricing, reserving, or capital. | +| `portfolio/flood.parquet` | Per-location CRC flood evaluation. | +| `comparison-inventory.json` | A file-shape smoke test beside synthetic VELO-shaped JSON—not a live, target-aligned comparison. | + +To change the geography, have the AI resolve and show bounds, choose EFAS or +GloFAS, and pass both to the runner. For example: + +```shell +./ai-playbooks/examples/run-open-demo.sh \ + glofas -79.42 43.63 -79.36 43.68 \ + pipeline_output/ai-playbooks-toronto +``` + +For a decision-facing assessment, replace Overture candidates with an authorized +asset or exposure export while retaining the canonical hazard acquisition and +provenance checks. + +## Why run the enterprise twin? + +CRC provides a transparent, extensible baseline over open or user-authorized +data. VELO/CDT adds proprietary asset and ownership intelligence, broader and +finer hazard coverage, future pathways and horizons, multi-factor modelling, +aggregation, and benchmarks. + +The comparison playbook keeps the relationship credible: it aligns common +dimensions, labels capability lift, and refuses to equate metrics whose semantics +differ. CRC is the target open foundation for an increasing share of the +VELO/CDT pipeline; this repository does not claim complete implementation parity. + +## Go deeper: run and tune the implementation + +Use the local environment to inspect provenance, substitute data, tune +assumptions, extend a workflow, or understand how a playbook maps to CRC +primitives and VELO/CDT calls. + +Install the open examples: + +```shell +uv sync +``` + +Add proprietary `velo-sdk` examples when authorized: + +```shell +uv sync --extra velo +``` + +Notebooks expect their working directory to be `notebooks/`; headless pipelines +run from the repository root: ```shell uv run jupyter lab notebooks/ uv run python pipelines/asset_portfolio_pipeline.py ``` -## Learning path +### Areal / spatial analytics -Two tracks share the same packages: **areal / spatial analytics** and -**asset portfolios**. The portfolio track is ordered and uses the checked-in -Cologne fixtures under `fixtures/os_climate/`, so it runs without AWS access. - -### Areal / spatial +These workflows acquire and canonicalize hazard data for a selected geography. | Notebook | Pipeline twin | What it shows | |---|---|---| -| [`notebooks/flood_risk_by_province.ipynb`](notebooks/flood_risk_by_province.ipynb) | [`pipelines/flood_admin_pipeline.py`](pipelines/flood_admin_pipeline.py) | Fluent EFAS area selection → pinned AOI cache → canonical curves → H3/admin join → Overture places → PMTiles | -| [`notebooks/jrc_global_flood_hazard.ipynb`](notebooks/jrc_global_flood_hazard.ipynb) | [`pipelines/jrc_flood_pipeline.py`](pipelines/jrc_flood_pipeline.py) | Fluent GLOFAS area selection → pinned AOI cache → fitted curves → return-period evaluation | -| [`notebooks/jrc_edo_drought_index.ipynb`](notebooks/jrc_edo_drought_index.ipynb) | [`pipelines/jrc_drought_pipeline.py`](pipelines/jrc_drought_pipeline.py) | Fluent EDO SMI area/year selection → checksummed annual-minimum AOI cache → curated lower-tail curves → support-aware evaluation | +| [`notebooks/flood_risk_by_province.ipynb`](notebooks/flood_risk_by_province.ipynb) | [`pipelines/flood_admin_pipeline.py`](pipelines/flood_admin_pipeline.py) | EFAS area selection → pinned AOI cache → canonical curves → H3/admin join → Overture places → PMTiles | +| [`notebooks/jrc_global_flood_hazard.ipynb`](notebooks/jrc_global_flood_hazard.ipynb) | [`pipelines/jrc_flood_pipeline.py`](pipelines/jrc_flood_pipeline.py) | EFAS/GloFAS selection → pinned AOI cache → fitted curves → return-period evaluation | +| [`notebooks/jrc_edo_drought_index.ipynb`](notebooks/jrc_edo_drought_index.ipynb) | [`pipelines/jrc_drought_pipeline.py`](pipelines/jrc_drought_pipeline.py) | EDO SMI area/year selection → annual-minimum AOI cache → lower-tail curves → support-aware evaluation | + +### Asset portfolio analytics -### Asset portfolios +This ordered technical track uses checked-in Cologne fixtures so the core CRC +mechanics can be studied offline. Those fixtures are regression and learning +inputs; the user-facing AI playbooks above acquire Overture and JRC data. | # | Notebook | Pipeline twin | What it shows | |---|---|---|---| | 1 | [`notebooks/hurdle_fit_primer.ipynb`](notebooks/hurdle_fit_primer.ipynb) | — | Reconstruct a canonical hurdle + sample diagnostics | -| 2 | [`notebooks/asset_portfolio_evaluation.ipynb`](notebooks/asset_portfolio_evaluation.ipynb) | [`pipelines/asset_portfolio_pipeline.py`](pipelines/asset_portfolio_pipeline.py) | Fluent `HazardDataset` portfolio RP evaluation | -| 3 | [`notebooks/portfolio_impact.ipynb`](notebooks/portfolio_impact.ipynb) | [`pipelines/portfolio_impact_pipeline.py`](pipelines/portfolio_impact_pipeline.py) | Event-aligned `PiecewiseLinearImpact` → \$ damage | +| 2 | [`notebooks/asset_portfolio_evaluation.ipynb`](notebooks/asset_portfolio_evaluation.ipynb) | [`pipelines/asset_portfolio_pipeline.py`](pipelines/asset_portfolio_pipeline.py) | `HazardDataset` portfolio return-period evaluation | +| 3 | [`notebooks/portfolio_impact.ipynb`](notebooks/portfolio_impact.ipynb) | [`pipelines/portfolio_impact_pipeline.py`](pipelines/portfolio_impact_pipeline.py) | Event-aligned `PiecewiseLinearImpact` → damage ratio and derived currency damage | | 4 | [`notebooks/portfolio_risk_metrics.ipynb`](notebooks/portfolio_risk_metrics.ipynb) | [`pipelines/portfolio_risk_pipeline.py`](pipelines/portfolio_risk_pipeline.py) | Microscores → portfolio VaR / CVaR + attribution | | 5 | [`notebooks/multi_scenario_portfolio.ipynb`](notebooks/multi_scenario_portfolio.ipynb) | [`pipelines/multi_scenario_pipeline.py`](pipelines/multi_scenario_pipeline.py) | Historical vs transparent local tail stresses in one evaluation | -## Notes +Change the geography, portfolio, canonical hazard input, impact function, or +scenario assumptions while preserving provenance, units, probability semantics, +coverage diagnostics, and non-equivalence warnings. + +## Design, gaps, and verification + +These are specialist references rather than required reading for a first run: + +- [Product strategy](ai-playbooks/docs/product-strategy.md): open foundation, + enterprise lift, and domain prioritization. +- [Capability matrix](ai-playbooks/docs/capability-matrix.md): verified open and + proprietary surfaces plus important non-equivalences. +- [Gap backlog](ai-playbooks/docs/gap-backlog.md): low-hanging CRC and VELO/CDT + workflow improvements. +- [Dogfood verification](ai-playbooks/docs/dogfood-verification.md): target + flexibility, regression coverage, and live open-bootstrap evidence. -- The portfolio track reads checked-in Parquet fixtures and is fully offline. - Its multi-scenario cases are sensitivity stresses, not calibrated projections. -- The areal track still fetches public boundary / raster inputs from - geoBoundaries, JRC, and Overture. -- Cached notebook outputs under `notebooks/data/` are gitignored. -- Pipeline outputs land in `pipeline_output/` (also gitignored). -- GitHub's notebook preview cannot run JS, so Plotly figures emit a static PNG - fallback via kaleido when notebooks are executed locally before commit. +Cached notebook outputs under `notebooks/data/` and pipeline outputs under +`pipeline_output/` are gitignored. GitHub cannot execute notebook JavaScript, so +locally executed Plotly figures include a static PNG fallback through Kaleido. diff --git a/ai-playbooks/docs/capability-matrix.md b/ai-playbooks/docs/capability-matrix.md new file mode 100644 index 0000000..ec0886f --- /dev/null +++ b/ai-playbooks/docs/capability-matrix.md @@ -0,0 +1,26 @@ +# Capability matrix + +Status reflects the public package surfaces verified on 2026-08-18 plus the product context supplied for this project. + +| Capability | CRC open stack | VELO/CDT enterprise | Demonstration implication | +|---|---|---|---| +| Computation core | Distributions, explicit curve fitting, impact transforms, microscores, spanning sets, VaR/CVaR, attribution | Enterprise scores and impacts are delivered through VELO/CDT | Explain CRC as the open computational foundation and VELO/CDT as the industrialized data/product layer | +| Flood data | JRC GloFAS and EFAS lazy ingestion and canonicalization | Broader proprietary hazard catalogue and finer coverage | Pair every open flood screen with a multi-hazard enterprise run | +| Drought data | JRC EDO Soil Moisture Index workflow | Broader hazard/scenario coverage | Use CRC for transparent drought screening; use VELO/CDT when business impact needs multiple interacting factors | +| Arbitrary data | Canonical Parquet; GeoTIFF/COG, Zarr, NetCDF, DuckDB/Arrow and geometry primitives | API/MCP-managed datasets and assets | CRC can onboard customer/open data, but needs more high-level recipes and validation | +| OS-Climate | Provider code exists; project context says the feed is currently unplugged | Not required for the proprietary route | Mark unavailable until a live integration test passes | +| Asset input | Caller supplies assets, or AI sources Overture candidate locations by AOI/category for an open demonstration | Public and organization companies/assets, ownership, indexes | Overture supports geographic screening but does not establish ownership, materiality, collateral, value, or portfolio membership | +| Hazard breadth | Currently concrete public workflows for riverine flood and SMI drought | Product site states 50+ hazards and multi-factor scenario data | Do not imply CRC parity; show the delta explicitly | +| Scenario support | Canonical horizon/pathway fields; depends on source data | Pathways and decadal horizons exposed by SDK | Align scenarios before comparison; historical JRC results are not future-scenario equivalents | +| Spatial precision | H3 plus exact source-geometry refinement when WKB is present | Asset-level proprietary data and finer hazard granularity | Report `spatial_match` and source resolution in the baseline | +| Portfolio metrics | Low-level binary outcomes, spanning sets, VaR/CVaR; single-dataset portfolio evaluation | Company/index scores, asset scores, country/type aggregation, impact attribution | CRC needs a high-level multi-hazard portfolio workflow | +| Insurability | User-defined impact curves and transparent risk primitives | SDK exposes “uninsurable” (`cvar_95 >= 0.35`) and “stranded” (`cvar_95 >= 0.75`) company-asset lists | Treat VELO labels as platform classifications, not universal actuarial definitions | +| Evidence and provenance | Canonical hazard/evaluation metadata and pinned source caches | API responses; public SDK does not expose a complete evidence bundle | Add a cross-stack assessment manifest and version/method export | +| Authentication | None for public/local data unless the source requires it | API key / remote MCP | Never place credentials in skill content or outputs | + +## Important non-equivalences + +- JRC flood depth at a return period is not the same object as VELO DCR, expected impact, VaR, or CVaR. +- Historical open observations are not interchangeable with a future climate pathway/horizon. +- A user-supplied coordinate or Overture candidate is not automatically equivalent to VELO's resolved physical asset or ownership record. +- A custom depth-damage ratio is not the same as VELO's proprietary impact model. diff --git a/ai-playbooks/docs/dogfood-verification.md b/ai-playbooks/docs/dogfood-verification.md new file mode 100644 index 0000000..b3589bf --- /dev/null +++ b/ai-playbooks/docs/dogfood-verification.md @@ -0,0 +1,58 @@ +# AI skill dogfood verification + +Verified locally on 2026-08-18 with `crc-sdk==0.3.0`, `crc-framework==0.2.0`, and `velo-sdk==0.0.20`. + +Run the repeatable offline suite from the repository root: + +```shell +uv run --extra velo python ai-playbooks/examples/dogfood/verify_skills.py +``` + +The harness creates an isolated temporary directory and removes its generated assessments when it exits. CRC tests execute the real package workflows against checked-in or synthetic canonical hazard data. VELO tests execute the real bundled scripts against deterministic API doubles; they verify routing, target selection, scenario propagation, result shaping, and missing-equivalence guardrails without requiring credentials or calling a live service. + +The deterministic harness intentionally retains small offline regression data; +it is not the user-facing open bootstrap. The public playbooks use live Overture +candidate discovery and JRC acquisition through +`ai-playbooks/examples/run-open-demo.sh`. + +## Target-flexibility results + +| Skill | Target A | Target B | Result | +|---|---|---|---| +| `crc-screen-mortgage-flood` | EFAS / Cologne AOI | GloFAS / Toronto AOI | Pass: source and geographic bounds changed independently | +| `crc-model-flood-insurance-loss` | Historical/1980, one warehouse, 25/100-year periods | Synthetic-stress/2050, two assets, 50/500-year periods | Pass: a two-scenario fixture was filtered to exactly the requested pathway/horizon in each run | +| `crc-assess-asset-portfolio-risk` | One-asset flood book | Two-asset flood + synthetic drought book | Pass: portfolio size and hazard set changed | +| `velo-underwrite-property-climate` | Asset ID, Cologne warehouse, SSP2-4.5/2050 | Search-resolved Toronto plant, SSP5-8.5/2070 | Pass with API double: identity route, geography, scenario, owner, and scores changed | +| `velo-triage-portfolio-insurability` | Company A, SSP2-4.5/2050 | Company B, SSP5-8.5/2070 | Pass with API double: company/scenario changed and classifications followed the selected company | +| `velo-assess-company-climate-risk` | Single company | Market index | Pass with API double after adding explicit `--company-id` / `--index-id` routing | +| `compare-crc-velo-assessments` | Flood/property assessment pair | Drought/market-index assessment pair | Pass: both CRC and VELO artifact shapes changed while non-equivalence remained explicit | + +## Findings and corrections + +Dogfooding found one material flexibility gap: `velo-assess-company-climate-risk` described company and market-index analysis, but its script accepted only `--company-id`. The script now supports mutually exclusive `--company-id` and `--index-id` targets and dispatches to the corresponding VELO SDK methods. + +Review found that `crc-model-flood-insurance-loss` did not select pathway and +horizon before return-period evaluation. Its CLI now requires both values, and a +regression case constructs a two-scenario flood file and verifies that each run +contains only its selected scenario. + +## What this verifies + +- Skill instructions and scripts do not hard-code one company, asset, geography, pathway, horizon, portfolio size, return-period set, or damage curve. +- CRC scripts execute real local computations and preserve explicit hazard/scenario metadata. +- VELO scripts select and propagate different targets correctly at the SDK boundary. +- The comparison skill tolerates different enterprise artifact shapes without claiming metric equivalence. + +## Live open-bootstrap verification + +The networked Cologne bootstrap was also verified on 2026-08-18 for bounds +`6.95,50.93,6.97,50.95`. It resolved JRC EFAS release `3.1.1`, materialized 280 +canonical curve rows across 21 covered H3 cells, resolved Overture Places release +`2026-07-22.0`, selected five candidates inside JRC source geometry, and produced +five-row mortgage, flood-loss, and portfolio evaluations. This verifies the +acquisition seam; it does not turn those Overture candidates into a real business +portfolio. + +## Remaining live-system validation + +The API-double tests do not prove live VELO permissions, latency, pagination behavior, data availability, or server response compatibility. Before release, run the three VELO skills against an authorized sandbox using at least two real targets and record redacted response-schema fixtures. Do not use production mutation endpoints for this check. diff --git a/ai-playbooks/docs/gap-backlog.md b/ai-playbooks/docs/gap-backlog.md new file mode 100644 index 0000000..64d85fa --- /dev/null +++ b/ai-playbooks/docs/gap-backlog.md @@ -0,0 +1,38 @@ +# Gap backlog + +## CRC-side workflow gaps + +| Priority | Gap | Why it unlocks use cases | Suggested increment | +|---|---|---|---| +| P0 | Multi-hazard portfolio orchestration | Finance and insurance users currently must stitch single canonical datasets together | Add `PortfolioAssessment` over multiple `HazardDataset` objects with scenario alignment, missingness policy, and bounded branch computation | +| P0 | Assessment manifest and evidence export | Model-risk, compliance, and sales demos need one auditable artifact | Emit JSON containing package/data versions, hashes, filters, units, probability convention, impact functions, warnings, row counts, and unresolved assets | +| P0 | Dataset onboarding workflow | Zarr/GeoTIFF/Parquet primitives are capable but too low-level for new community datasets | Add `HazardDataset.from_raster/from_zarr/from_table` builders with schema validation, fit policy, coverage estimate, and conformance fixtures | +| P0 | Future open CDT adapter | Preserves one downstream workflow across open and proprietary fronts | Map the released dataset directly into the canonical hazard contract; publish checksummed example subsets | +| P1 | Vulnerability/damage-function packs | Mortgage and insurance demos otherwise depend on ad hoc curves | Add versioned, cited residential/commercial/industrial curves with geography/building context and uncertainty bounds | +| P1 | Scenario/horizon crosswalk | Open sources and CDT pathways may not align | Add explicit scenario vocabulary, equivalence rules, and “not comparable” diagnostics | +| P1 | High-level decision playbooks | Users can calculate values but still need domain outputs | Add mortgage screen, insurance loss, corporate asset review, and compliance evidence workflow modules | +| P1 | Data quality and coverage report | Missing assets and spatial approximations can be mistaken for low risk | Add coverage, duplicate, exact-vs-H3, extrapolation, and fit-quality summaries | +| P2 | Geocoding and entity/ownership adapters | Required to move from borrower/company names to physical assets | Keep optional and modular; integrate open geocoders/entity data without making them core dependencies | +| P2 | Calibration/backtesting harness | Decision users need evidence that transforms and thresholds work | Add observed-event and claims/loss backtests with leakage-safe splits and versioned metrics | + +## VELO/CDT-side gaps + +| Priority | Gap | Demonstration impact | Suggested increment | +|---|---|---|---| +| P0 | Direct asset score helper | Public `velo-sdk` has asset lookup but climate scores are reached through company asset listings | Add `assets.get_climate_scores(asset_id, pathway, horizon)` and impact equivalent | +| P0 | Assessment/evidence export | Enterprise results need reproducibility and governance | Return method/data/model versions, query parameters, asset snapshot timestamp, warnings, and stable export IDs | +| P0 | MCP/SDK capability parity | Skills need deterministic discovery across tools | Publish a capability manifest and keep MCP tool names/arguments mapped to SDK methods | +| P1 | CRC canonical output crosswalk | Makes paired demos and customer migration credible | Export an optional normalized assessment table keyed by asset, hazard/index, pathway, horizon, metric, and unit | +| P1 | Read-only upload preview and job status | Organization asset onboarding is mutation-heavy and asynchronous | Add validation/preview, explicit commit, job status, and idempotency keys | +| P1 | Coverage/exclusion diagnostics | “No score” must not look like “no risk” | Return missing hazards, low-confidence assets, geocoding ambiguity, and unavailable scenarios | +| P2 | Bring-your-own-hazard through CRC | Lets customers combine proprietary coverage with internal hazards | Accept canonical CRC datasets into controlled enterprise workflows with lineage isolation | + +## Quality gates for all demos + +1. No silent asset drops. +2. No silent return-period extrapolation. +3. No historical/future scenario substitution. +4. No score-to-decision threshold without owner-approved policy. +5. No live upload, company creation, or external write without explicit authorization. +6. Every result includes source, version, timestamp, units, assumptions, and limitations. + diff --git a/ai-playbooks/docs/product-strategy.md b/ai-playbooks/docs/product-strategy.md new file mode 100644 index 0000000..11ad43c --- /dev/null +++ b/ai-playbooks/docs/product-strategy.md @@ -0,0 +1,56 @@ +# Product strategy: open foundation, enterprise lift + +## Positioning + +Lead with an auditable open baseline, then demonstrate the decision lift from VELO/CDT on the same business question. The message is not “open is a teaser.” It is: + +1. CRC is the transparent computation and workflow foundation. +2. The community can bring open or owned hazard data and inspect every modelling choice. +3. VELO/CDT industrializes that foundation with broader and finer hazard coverage, proprietary physical-asset and ownership data, multi-factor scenarios, and enterprise-ready scores. +4. A paired assessment shows which decisions can be made with the baseline and which need the enterprise lift. + +Avoid claiming that every current VELO/CDT component already runs on CRC. Use the accurate formulation: “CRC is the target open foundation and an increasing share of the VELO/CDT pipeline is being materialized on that stack.” + +## Demonstration pattern + +Every demo follows one narrative: + +1. Frame the decision: collateral review, underwriting triage, or investment diligence. +2. Run the CRC baseline on user-supplied assets or clearly labelled Overture candidate locations with JRC or another canonical hazard dataset. +3. Show the baseline's provenance, assumptions, coverage, unresolved assets, and extrapolation. +4. Run the VELO/CDT twin on the aligned asset/company/index and scenario. +5. Reconcile only comparable dimensions; label the rest as capability lift. +6. End with a decision memo and a next-data recommendation, not an opaque score. + +## Domain prioritization + +### 1. Mortgage and commercial real estate + +Best first demo. A single property is intuitive, JRC flood depth is explainable, and VELO can add proprietary asset identity, broader hazards, scenarios, and tail metrics. The open baseline can support screening; the enterprise twin is positioned for underwriting and portfolio governance. + +### 2. Property insurance + +Strong second demo. CRC makes the vulnerability assumption explicit through depth-damage functions and event-aligned impacts. VELO adds portfolio-level insurability/stranding triage and multi-factor attribution. Keep pricing and capital claims out of scope unless actuarial calibration is supplied. + +### 3. Corporate and investment climate diligence + +Strongest proprietary differentiation. CRC can assess a supplied asset book or AI-selected Overture candidate locations, but Overture does not prove company ownership or materiality. VELO adds companies, subsidiaries/assets, market indexes, country/type aggregation, and company/index risk metrics. + +### 4. Compliance and disclosure evidence + +Treat this as an output layer across the three domains rather than a standalone hazard calculation. CRC's self-describing canonical Parquet contract is well suited to reproducibility. VELO needs an exportable methodology/version/evidence bundle so enterprise results can enter model-risk and disclosure workflows. + +## Future open CDT hazard dataset + +Design now for a source adapter that produces the existing CRC canonical hazard Parquet contract. Do not fork the downstream workflow. When the dataset is released, the same portfolio, impact, and comparison skills should work by changing only the dataset acquisition step. + +The release-ready integration contract should include: + +- stable dataset and release identifiers; +- hazard name, value unit, and semantics; +- pathway and horizon conventions; +- non-exceedance probability convention and source return-period support; +- source resolution, H3 resolution, and geometry precision; +- license and redistribution terms; +- checksums, producer version, and creation time; +- an adapter conformance fixture with expected canonical rows and quantiles. diff --git a/ai-playbooks/examples/dogfood/verify_skills.py b/ai-playbooks/examples/dogfood/verify_skills.py new file mode 100644 index 0000000..b1cb861 --- /dev/null +++ b/ai-playbooks/examples/dogfood/verify_skills.py @@ -0,0 +1,430 @@ +#!/usr/bin/env python3 +"""Offline dogfood suite for target flexibility across all bundled Agent Skills.""" + +from __future__ import annotations + +import argparse +import csv +import importlib.util +import json +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import Any + +import pyarrow as pa +import pyarrow.parquet as pq + +from crc_sdk.connectors import read_hazard_dataset, read_hazard_metadata +from crc_sdk.connectors.parquet import hazard_arrow_schema, write_hazard_dataset +from crc_sdk.types import SourceProvenance + +sys.dont_write_bytecode = True + +ROOT = Path(__file__).resolve().parents[3] +SKILLS = ROOT / ".agents" / "skills" +HAZARD_FIXTURE = ROOT / "fixtures" / "os_climate" / "hazard.parquet" + + +class Record: + """Small Pydantic-like object for deterministic VELO API doubles.""" + + def __init__(self, **values: Any) -> None: + self.__dict__.update(values) + + def model_dump(self, **_: Any) -> dict[str, Any]: + def convert(value: Any) -> Any: + if isinstance(value, Record): + return value.model_dump() + if isinstance(value, list): + return [convert(item) for item in value] + return value + + return {key: convert(value) for key, value in self.__dict__.items()} + + +class FakeClimate: + def list_pathways(self) -> list[str]: + return ["ssp245", "ssp585"] + + def list_horizons(self) -> list[int]: + return [2050, 2070] + + +class FakeAssets: + def __init__(self) -> None: + self.items = { + "asset-a": Record( + id="asset-a", + name="Alpha Warehouse", + address="1 Alpha Road", + latitude=50.9375, + longitude=6.9603, + asset_type="warehouse", + ), + "asset-b": Record( + id="asset-b", + name="Beta Plant", + address="2 Beta Road", + latitude=43.65, + longitude=-79.38, + asset_type="factory", + ), + } + + def get_asset(self, asset_id: str) -> Record: + return self.items[asset_id] + + def search_assets(self, query: str) -> list[Record]: + query = query.lower() + return [item for item in self.items.values() if query in item.name.lower()] + + def get_asset_owner(self, asset_id: str) -> Record: + suffix = asset_id[-1] + return Record(id=f"company-{suffix}", name=f"Company {suffix.upper()}") + + +class FakeCompanies: + def get_company(self, company_id: str) -> Record: + return Record(id=company_id, name=f"Company {company_id[-1].upper()}") + + def list_company_asset_climate_scores( + self, company_id: str, pathway: str, horizon: int + ) -> list[Record]: + suffix = company_id[-1] + return [ + Record( + asset_id=f"asset-{suffix}", + dcr_score=0.2 if suffix == "a" else 0.6, + cvar_95=0.3 if suffix == "a" else 0.8, + pathway=pathway, + horizon=horizon, + ) + ] + + def list_company_asset_impact_scores( + self, company_id: str, pathway: str, horizon: int + ) -> list[Record]: + suffix = company_id[-1] + return [ + Record( + asset_id=f"asset-{suffix}", + index_risks=[ + Record( + index_name="flood" if suffix == "a" else "heat", + index_impact_cvar_50=0.1 if suffix == "a" else 0.4, + ) + ], + pathway=pathway, + horizon=horizon, + ) + ] + + def list_uninsurable_company_assets( + self, company_id: str, _pathway: str, _horizon: int + ) -> list[Record]: + return [] if company_id.endswith("a") else [Record(asset_id="asset-b", cvar_95=0.8)] + + def list_stranded_company_assets( + self, company_id: str, _pathway: str, _horizon: int + ) -> list[Record]: + return [] if company_id.endswith("a") else [Record(asset_id="asset-b", cvar_95=0.8)] + + def get_company_climate_scores( + self, company_id: str, _pathway: str, _horizon: int + ) -> Record: + return Record(dcr_score=0.25 if company_id.endswith("a") else 0.65, cvar_95=0.3) + + def get_company_impact_scores( + self, company_id: str, _pathway: str, _horizon: int + ) -> list[Record]: + return [Record(index_name="flood", index_impact_cvar_95=0.1 if company_id.endswith("a") else 0.5)] + + def aggregate_company_asset_climate_scores_by_country( + self, company_id: str, _pathway: str, _horizon: int + ) -> list[Record]: + return [Record(country="DEU" if company_id.endswith("a") else "CAN", asset_count=1, dcr_score=0.4)] + + def aggregate_company_asset_climate_scores_by_asset_type( + self, company_id: str, _pathway: str, _horizon: int + ) -> list[Record]: + return [Record(asset_type="warehouse" if company_id.endswith("a") else "factory", asset_count=1, dcr_score=0.4)] + + +class FakeMarkets: + def get_index(self, index_id: str) -> Record: + return Record(id=index_id, name="Synthetic Industrial Index") + + def get_index_climate_scores( + self, _index_id: str, _pathway: str, _horizon: int + ) -> Record: + return Record(dcr_score=0.55, cvar_95=0.5) + + def get_index_impact_scores( + self, _index_id: str, _pathway: str, _horizon: int + ) -> list[Record]: + return [Record(index_name="heat", index_impact_cvar_95=0.35)] + + def aggregate_index_asset_climate_scores_by_country( + self, _index_id: str, _pathway: str, _horizon: int + ) -> list[Record]: + return [Record(country="USA", asset_count=20, dcr_score=0.55)] + + def aggregate_index_asset_climate_scores_by_asset_type( + self, _index_id: str, _pathway: str, _horizon: int + ) -> list[Record]: + return [Record(asset_type="factory", asset_count=20, dcr_score=0.55)] + + +class FakeClient: + def __init__(self) -> None: + self.climate = FakeClimate() + self.assets = FakeAssets() + self.companies = FakeCompanies() + self.markets = FakeMarkets() + + +def command(*parts: str | Path) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [str(part) for part in parts], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ) + + +def load_script(skill: str, filename: str) -> Any: + path = SKILLS / skill / "scripts" / filename + spec = importlib.util.spec_from_file_location(f"dogfood_{skill.replace('-', '_')}", path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot load {path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def run_mocked(module: Any, argv: list[str], client: FakeClient) -> None: + old_argv = sys.argv + old_client = module.APIClient + try: + module.APIClient = lambda: client + sys.argv = [str(module.__file__), *argv] + result = module.main() + if result not in (None, 0): + raise AssertionError(f"unexpected return code {result}") + finally: + module.APIClient = old_client + sys.argv = old_argv + + +def write_asset_targets(directory: Path) -> tuple[Path, Path]: + rows = [ + { + "asset_id": "synthetic-dogfood-a", + "longitude": "6.9603", + "latitude": "50.9375", + "asset_name": "Synthetic Cologne A", + }, + { + "asset_id": "synthetic-dogfood-b", + "longitude": "6.96031", + "latitude": "50.93751", + "asset_name": "Synthetic Cologne B", + }, + { + "asset_id": "synthetic-dogfood-c", + "longitude": "6.96029", + "latitude": "50.93749", + "asset_name": "Synthetic Cologne C", + }, + ] + fieldnames = list(rows[0]) + + first = directory / "portfolio-alpha.csv" + second = directory / "portfolio-beta.csv" + for path, selected in ((first, rows[:1]), (second, rows[1:])): + with path.open("w", newline="") as target: + writer = csv.DictWriter(target, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(selected) + return first, second + + +def synthetic_drought(destination: Path) -> Path: + source = read_hazard_dataset(HAZARD_FIXTURE) + metadata = read_hazard_metadata(HAZARD_FIXTURE) + rows = [] + for original in source.to_pylist(): + row = dict(original) + row["hazard_name"] = "SyntheticDroughtIndex" + row["source_id"] = f"{row['source_id']}-synthetic-drought" + rows.append(row) + drought_metadata = metadata.model_copy( + update={ + "return_period_tail": "lower", + "value_unit": "index", + "value_semantics": "synthetic drought index for dogfood only", + "producer": "crc-docs-dogfood", + "source": SourceProvenance( + provider="synthetic", + dataset="drought-flexibility-fixture", + version="1", + ), + } + ) + table = pa.Table.from_pylist(rows, schema=hazard_arrow_schema(drought_metadata)) + write_hazard_dataset(table, destination, drought_metadata, max_workers=1) + return destination + + +def multi_scenario_flood(destination: Path) -> Path: + source = read_hazard_dataset(HAZARD_FIXTURE) + metadata = read_hazard_metadata(HAZARD_FIXTURE) + rows = source.to_pylist() + for original in source.to_pylist(): + row = dict(original) + row["pathway"] = "synthetic-stress" + row["horizon"] = 2050 + row["curve_location"] += 0.75 + rows.append(row) + scenario_metadata = metadata.model_copy( + update={ + "producer": "crc-docs-dogfood", + "source": SourceProvenance( + provider="synthetic", + dataset="multi-scenario-flood-selection-fixture", + version="1", + ), + } + ) + table = pa.Table.from_pylist(rows, schema=hazard_arrow_schema(scenario_metadata)) + write_hazard_dataset(table, destination, scenario_metadata, max_workers=1) + return destination + + +def dogfood_mortgage(directory: Path, assets: Path) -> dict[str, Any]: + script = SKILLS / "crc-screen-mortgage-flood" / "scripts" / "screen_mortgage_flood.py" + common = [sys.executable, script, "--assets", assets, "--cache", directory / "cache", "--output", directory / "unused.parquet", "--plan-only"] + europe = command(*common, "--dataset", "efas", "--bounds", "6.8", "50.8", "7.7", "51.1") + canada = command(*common, "--dataset", "glofas", "--bounds", "-79.7", "43.5", "-79.1", "43.9") + assert "cems-efas" in europe.stdout and "cems-glofas" in canada.stdout + return {"skill": "crc-screen-mortgage-flood", "targets": ["EFAS/Cologne AOI", "GloFAS/Toronto AOI"], "result": "pass"} + + +def dogfood_loss(directory: Path, portfolios: tuple[Path, Path]) -> dict[str, Any]: + script = SKILLS / "crc-model-flood-insurance-loss" / "scripts" / "model_flood_loss.py" + hazard = multi_scenario_flood(directory / "multi-scenario-flood.parquet") + outputs = [directory / "loss-alpha.parquet", directory / "loss-beta.parquet"] + command(sys.executable, script, "--hazard", hazard, "--assets", portfolios[0], "--output", outputs[0], "--periods", "25", "100", "--pathway", "historical", "--horizon", "1980", "--depth-knots", "0,0.2,1,2", "--damage-knots", "0,0,0.25,1", "--workers", "1") + command(sys.executable, script, "--hazard", hazard, "--assets", portfolios[1], "--output", outputs[1], "--periods", "50", "500", "--pathway", "synthetic-stress", "--horizon", "2050", "--depth-knots", "0,0.5,1.5,3", "--damage-knots", "0,0.1,0.5,1", "--workers", "1") + alpha = pq.read_table(outputs[0]) + beta = pq.read_table(outputs[1]) + assert alpha.num_rows == 1 and set(alpha["pathway"].to_pylist()) == {"historical"} and set(alpha["horizon"].to_pylist()) == {1980} + assert beta.num_rows == 2 and set(beta["pathway"].to_pylist()) == {"synthetic-stress"} and set(beta["horizon"].to_pylist()) == {2050} + return {"skill": "crc-model-flood-insurance-loss", "targets": ["historical/1980 single warehouse", "synthetic-stress/2050 two assets"], "result": "pass"} + + +def dogfood_portfolio(directory: Path, portfolios: tuple[Path, Path]) -> tuple[dict[str, Any], Path, Path]: + script = SKILLS / "crc-assess-asset-portfolio-risk" / "scripts" / "assess_asset_portfolio.py" + drought = synthetic_drought(directory / "synthetic-drought.parquet") + output_a = directory / "portfolio-alpha" + output_b = directory / "portfolio-beta" + command(sys.executable, script, "--assets", portfolios[0], "--hazard", f"flood={HAZARD_FIXTURE}", "--output-dir", output_a, "--periods", "25", "100", "--pathway", "historical", "--horizon", "1980", "--workers", "1") + command(sys.executable, script, "--assets", portfolios[1], "--hazard", f"flood={HAZARD_FIXTURE}", "--hazard", f"drought={drought}", "--output-dir", output_b, "--periods", "50", "500", "--pathway", "historical", "--horizon", "1980", "--workers", "1") + assert pq.read_table(output_a / "flood.parquet").num_rows == 1 + assert pq.read_table(output_b / "flood.parquet").num_rows == 2 + assert pq.read_table(output_b / "drought.parquet").num_rows == 2 + evidence = {"skill": "crc-assess-asset-portfolio-risk", "targets": ["one-asset flood book", "two-asset flood+drought book"], "result": "pass"} + return evidence, output_a / "flood.parquet", output_b / "drought.parquet" + + +def dogfood_property(directory: Path, client: FakeClient) -> tuple[dict[str, Any], Path, Path]: + module = load_script("velo-underwrite-property-climate", "underwrite_property.py") + output_a = directory / "property-alpha.json" + output_b = directory / "property-beta.json" + run_mocked(module, ["--asset-id", "asset-a", "--pathway", "ssp245", "--horizon", "2050", "--output", str(output_a)], client) + run_mocked(module, ["--query", "Beta", "--pathway", "ssp585", "--horizon", "2070", "--output", str(output_b)], client) + alpha = json.loads(output_a.read_text()) + beta = json.loads(output_b.read_text()) + assert alpha["asset"]["id"] == "asset-a" and beta["asset"]["id"] == "asset-b" + evidence = {"skill": "velo-underwrite-property-climate", "targets": ["asset ID/Cologne warehouse/2050", "search/Toronto plant/2070"], "result": "pass (API double)"} + return evidence, output_a, output_b + + +def dogfood_insurability(directory: Path, client: FakeClient) -> dict[str, Any]: + module = load_script("velo-triage-portfolio-insurability", "triage_insurability.py") + output_a = directory / "insurability-alpha.json" + output_b = directory / "insurability-beta.json" + run_mocked(module, ["--company-id", "company-a", "--pathway", "ssp245", "--horizon", "2050", "--output", str(output_a)], client) + run_mocked(module, ["--company-id", "company-b", "--pathway", "ssp585", "--horizon", "2070", "--output", str(output_b)], client) + alpha = json.loads(output_a.read_text()) + beta = json.loads(output_b.read_text()) + assert alpha["counts"]["uninsurable"] == 0 and beta["counts"]["uninsurable"] == 1 + return {"skill": "velo-triage-portfolio-insurability", "targets": ["Company A/2050", "Company B/2070"], "result": "pass (API double)"} + + +def dogfood_company(directory: Path, client: FakeClient) -> tuple[dict[str, Any], Path]: + module = load_script("velo-assess-company-climate-risk", "assess_company.py") + company_output = directory / "company.json" + index_output = directory / "index.json" + run_mocked(module, ["--company-id", "company-a", "--pathway", "ssp245", "--horizon", "2050", "--output", str(company_output)], client) + run_mocked(module, ["--index-id", "index-industrials", "--pathway", "ssp585", "--horizon", "2070", "--output", str(index_output)], client) + company = json.loads(company_output.read_text()) + index = json.loads(index_output.read_text()) + assert company["target_type"] == "company" and index["target_type"] == "market_index" + evidence = {"skill": "velo-assess-company-climate-risk", "targets": ["single company", "market index"], "result": "pass (API double)"} + return evidence, index_output + + +def dogfood_compare(directory: Path, crc_outputs: tuple[Path, Path], velo_outputs: tuple[Path, Path]) -> dict[str, Any]: + script = SKILLS / "compare-crc-velo-assessments" / "scripts" / "inventory_assessments.py" + first = directory / "compare-property.json" + second = directory / "compare-index.json" + command(sys.executable, script, "--crc", crc_outputs[0], "--velo", velo_outputs[0], "--output", first) + command(sys.executable, script, "--crc", crc_outputs[1], "--velo", velo_outputs[1], "--output", second) + first_result = json.loads(first.read_text()) + second_result = json.loads(second.read_text()) + assert first_result["classification"] == second_result["classification"] == "inventory_only" + assert first_result["velo"]["top_level_fields"] != second_result["velo"]["top_level_fields"] + return {"skill": "compare-crc-velo-assessments", "targets": ["flood/property pair", "drought/market-index pair"], "result": "pass"} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--report", type=Path, help="Optional JSON evidence output") + args = parser.parse_args() + + for skill in SKILLS.iterdir(): + if skill.is_dir() and not (skill / "SKILL.md").is_file(): + raise AssertionError(f"missing SKILL.md: {skill}") + + with tempfile.TemporaryDirectory(prefix="crc-skill-dogfood-") as raw_directory: + directory = Path(raw_directory) + portfolios = write_asset_targets(directory) + evidence: list[dict[str, Any]] = [] + evidence.append(dogfood_mortgage(directory, portfolios[0])) + evidence.append(dogfood_loss(directory, portfolios)) + portfolio_evidence, crc_a, crc_b = dogfood_portfolio(directory, portfolios) + evidence.append(portfolio_evidence) + + client = FakeClient() + property_evidence, property_a, _property_b = dogfood_property(directory, client) + evidence.append(property_evidence) + evidence.append(dogfood_insurability(directory, client)) + company_evidence, index_output = dogfood_company(directory, client) + evidence.append(company_evidence) + evidence.append(dogfood_compare(directory, (crc_a, crc_b), (property_a, index_output))) + + report = {"status": "pass", "skills_tested": len(evidence), "evidence": evidence} + rendered = json.dumps(report, indent=2, sort_keys=True) + "\n" + if args.report: + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text(rendered) + print(rendered, end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/ai-playbooks/examples/run-open-demo.sh b/ai-playbooks/examples/run-open-demo.sh new file mode 100755 index 0000000..199095a --- /dev/null +++ b/ai-playbooks/examples/run-open-demo.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "$script_dir/../.." && pwd)" + +if [[ $# -ne 0 && $# -ne 5 && $# -ne 6 ]]; then + echo "usage: $0 [efas|glofas MIN_LON MIN_LAT MAX_LON MAX_LAT [OUTPUT_DIR]]" >&2 + exit 2 +fi + +dataset="${1:-efas}" +min_lon="${2:-6.95}" +min_lat="${3:-50.93}" +max_lon="${4:-6.97}" +max_lat="${5:-50.95}" +output_dir="${6:-$repo_root/pipeline_output/ai-playbooks}" + +if [[ "$dataset" != "efas" && "$dataset" != "glofas" ]]; then + echo "dataset must be efas or glofas" >&2 + exit 2 +fi + +cd "$repo_root" +mkdir -p "$output_dir" + +echo "Materializing canonical JRC $dataset flood data for the selected bounds..." +uv run python pipelines/jrc_flood_pipeline.py \ + --dataset "$dataset" \ + --bounds "$min_lon" "$min_lat" "$max_lon" "$max_lat" \ + --return-periods 25 100 500 \ + --output-dir "$output_dir" + +echo "Sourcing Overture candidates inside the JRC coverage..." +uv run python pipelines/overture_assets_pipeline.py \ + --bounds "$min_lon" "$min_lat" "$max_lon" "$max_lat" \ + --coverage-hazard "$output_dir/jrc_depths_by_cell.parquet" \ + --limit 5 \ + --output "$output_dir/overture-assets.csv" + +echo "Screening the Overture candidates against canonical JRC flood data..." +uv run python .agents/skills/crc-screen-mortgage-flood/scripts/screen_mortgage_flood.py \ + --assets "$output_dir/overture-assets.csv" \ + --hazard "$output_dir/jrc_depths_by_cell.parquet" \ + --output "$output_dir/mortgage-flood.parquet" \ + --periods 25 100 500 \ + --workers 1 + +echo "Running the offline illustrative insurance-loss assessment..." +uv run python .agents/skills/crc-model-flood-insurance-loss/scripts/model_flood_loss.py \ + --assets "$output_dir/overture-assets.csv" \ + --hazard "$output_dir/jrc_depths_by_cell.parquet" \ + --pathway historical \ + --horizon 0 \ + --depth-knots 0,0.2,1,2 \ + --damage-knots 0,0,0.25,1 \ + --periods 25 100 250 500 \ + --output "$output_dir/flood-loss.parquet" + +echo "Running the offline CRC asset-portfolio assessment..." +uv run python .agents/skills/crc-assess-asset-portfolio-risk/scripts/assess_asset_portfolio.py \ + --assets "$output_dir/overture-assets.csv" \ + --hazard flood="$output_dir/jrc_depths_by_cell.parquet" \ + --periods 25 100 500 \ + --pathway historical \ + --horizon 0 \ + --output-dir "$output_dir/portfolio" + +echo "Inventorying CRC output beside a clearly synthetic VELO-shaped example..." +uv run python .agents/skills/compare-crc-velo-assessments/scripts/inventory_assessments.py \ + --crc "$output_dir/flood-loss.parquet" \ + --velo ai-playbooks/examples/velo-company-example.json \ + --output "$output_dir/comparison-inventory.json" + +echo "Demo complete. Outputs: $output_dir" diff --git a/ai-playbooks/examples/velo-company-example.json b/ai-playbooks/examples/velo-company-example.json new file mode 100644 index 0000000..12f6bbb --- /dev/null +++ b/ai-playbooks/examples/velo-company-example.json @@ -0,0 +1,22 @@ +{ + "sdk_version": "0.0.20", + "target_type": "company", + "target": { + "id": "demo-company", + "name": "Synthetic Manufacturing Co." + }, + "pathway": "ssp585", + "horizon": 2050, + "climate_score": { + "dcr_score": 0.42, + "expected_impact": 0.08, + "cvar_95": 0.31 + }, + "factor_impacts": [ + { + "index_name": "synthetic-flood-factor", + "index_impact_cvar_95": 0.12 + } + ], + "example_only": true +} diff --git a/pipelines/jrc_flood_pipeline.py b/pipelines/jrc_flood_pipeline.py index 99652c7..8e3d8f1 100644 --- a/pipelines/jrc_flood_pipeline.py +++ b/pipelines/jrc_flood_pipeline.py @@ -32,6 +32,12 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--return-periods", type=int, nargs="+", default=[10, 50, 100, 500] ) + parser.add_argument( + "--dataset", + choices=("efas", "glofas"), + default="glofas", + help="Use EFAS for Europe and GloFAS for global coverage", + ) parser.add_argument("--h3-resolution", type=int, default=9) parser.add_argument( "--cache-mode", choices=("reuse", "offline", "refresh"), default="reuse" @@ -46,7 +52,7 @@ def main() -> None: output_dir.mkdir(parents=True, exist_ok=True) plan = ( - HazardDataset.glofas(version="latest") + getattr(HazardDataset, args.dataset)(version="latest") .for_area(tuple(args.bounds)) .cache(output_dir / "jrc-source-cache", mode=args.cache_mode) .source_periods("all") diff --git a/pipelines/overture_assets_pipeline.py b/pipelines/overture_assets_pipeline.py new file mode 100644 index 0000000..f3d2ad4 --- /dev/null +++ b/pipelines/overture_assets_pipeline.py @@ -0,0 +1,168 @@ +"""Source open candidate asset locations from the latest Overture Places release. + +Run with: + + uv run python pipelines/overture_assets_pipeline.py \ + --bounds 6.95 50.93 6.97 50.95 \ + --output pipeline_output/ai-playbooks/overture-assets.csv + +The output is suitable for open climate-risk demonstrations. Overture places are +not evidence of ownership, collateral status, occupancy, replacement value, or +insurance exposure. +""" + +from __future__ import annotations + +import argparse +import json +import urllib.request +from pathlib import Path +from typing import Any + +import pyarrow.csv as pacsv + +from crc_sdk.connectors.duckdb import DuckDBConnection, sql_quote + +OVERTURE_CATALOG_URL = "https://stac.overturemaps.org/catalog.json" + + +def read_json(url: str) -> dict[str, Any]: + with urllib.request.urlopen( # noqa: S310 - fixed HTTPS STAC host + url, timeout=30 + ) as response: + return json.load(response) + + +def intersects(left: tuple[float, float, float, float], right: list[float]) -> bool: + return not ( + right[2] < left[0] + or right[0] > left[2] + or right[3] < left[1] + or right[1] > left[3] + ) + + +def place_assets(bounds: tuple[float, float, float, float]) -> tuple[str, list[str]]: + catalog = read_json(OVERTURE_CATALOG_URL) + release = str(catalog["latest"]) + collection_url = ( + f"https://stac.overturemaps.org/{release}/places/place/collection.json" + ) + collection = read_json(collection_url) + assets = [] + for link in collection["links"]: + if link.get("rel") != "item": + continue + item = read_json(link["href"]) + if intersects(bounds, item["bbox"]): + assets.append(item["assets"]["aws"]["href"]) + if not assets: + raise SystemExit(f"no Overture Places partitions intersect bounds {bounds}") + return release, assets + + +def parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument( + "--bounds", + nargs=4, + required=True, + type=float, + metavar=("MIN_LON", "MIN_LAT", "MAX_LON", "MAX_LAT"), + ) + p.add_argument("--output", required=True, type=Path) + p.add_argument( + "--coverage-hazard", + type=Path, + help="Optional canonical hazard Parquet; retain only places inside a source geometry", + ) + p.add_argument("--limit", type=int, default=10) + p.add_argument("--min-confidence", type=float, default=0.8) + p.add_argument( + "--category", + action="append", + help="Optional exact Overture primary category; repeatable", + ) + p.add_argument( + "--allow-unnamed", + action="store_true", + help="Include place records without a primary name", + ) + return p + + +def main() -> int: + args = parser().parse_args() + bounds = tuple(args.bounds) + if not ( + -180 <= bounds[0] < bounds[2] <= 180 + and -90 <= bounds[1] < bounds[3] <= 90 + ): + raise SystemExit("bounds must be ordered WGS84 min_lon min_lat max_lon max_lat") + if args.limit < 1: + raise SystemExit("limit must be positive") + if not 0 <= args.min_confidence <= 1: + raise SystemExit("min-confidence must be within [0, 1]") + if args.coverage_hazard and not args.coverage_hazard.is_file(): + raise SystemExit(f"coverage hazard not found: {args.coverage_hazard}") + + release, urls = place_assets(bounds) + quoted_urls = ", ".join(sql_quote(url) for url in urls) + conditions = [ + f"bbox.xmin BETWEEN {bounds[0]} AND {bounds[2]}", + f"bbox.ymin BETWEEN {bounds[1]} AND {bounds[3]}", + f"confidence >= {args.min_confidence}", + ] + if not args.allow_unnamed: + conditions.append("names.primary IS NOT NULL") + if args.category: + categories = ", ".join(sql_quote(value) for value in args.category) + conditions.append(f"categories.primary IN ({categories})") + if args.coverage_hazard: + coverage_path = sql_quote(str(args.coverage_hazard.resolve())) + conditions.append( + "EXISTS (SELECT 1 FROM read_parquet(" + f"{coverage_path}) AS h WHERE " + "ST_Intersects(ST_GeomFromWKB(h.source_geometry), p.geometry))" + ) + + args.output.parent.mkdir(parents=True, exist_ok=True) + con = DuckDBConnection.for_analytics(args.output.parent).connect() + query = f""" + SELECT + 'overture-place:' || id AS asset_id, + bbox.xmin AS longitude, + bbox.ymin AS latitude, + names.primary AS asset_name, + categories.primary AS overture_category, + confidence AS overture_confidence, + operating_status, + id AS overture_record_id, + '{release}' AS overture_release, + 'Overture Maps Foundation' AS overture_source, + 'https://docs.overturemaps.org/attribution/' AS overture_attribution + FROM read_parquet([{quoted_urls}]) AS p + WHERE {' AND '.join(conditions)} + ORDER BY confidence DESC, id + LIMIT {args.limit} + """ + table = con.execute(query).to_arrow_table() + if table.num_rows == 0: + raise SystemExit( + "no Overture places matched; widen bounds, lower min-confidence, " + "or remove category filters" + ) + pacsv.write_csv(table, args.output) + print( + f"wrote {table.num_rows} Overture candidate locations from release " + f"{release} -> {args.output}" + ) + print( + "warning: candidates are not verified ownership, collateral, occupancy, " + "replacement value, or insurance exposure" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyproject.toml b/pyproject.toml index 679a910..5eafbfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,3 +11,8 @@ dependencies = [ "plotly>=6.9.0", "crc-sdk[zarr,raster,netcdf,geometry]>=0.3.0", ] + +[project.optional-dependencies] +velo = [ + "velo-sdk==0.0.20", +] diff --git a/uv.lock b/uv.lock index f23de84..72b499c 100644 --- a/uv.lock +++ b/uv.lock @@ -6,10 +6,10 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version < '3.13' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] @@ -349,6 +349,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + [[package]] name = "beautifulsoup4" version = "4.15.0" @@ -616,6 +625,11 @@ dependencies = [ { name = "plotly" }, ] +[package.optional-dependencies] +velo = [ + { name = "velo-sdk" }, +] + [package.metadata] requires-dist = [ { name = "crc-sdk", extras = ["zarr", "raster", "netcdf", "geometry"], specifier = ">=0.3.0" }, @@ -623,7 +637,9 @@ requires-dist = [ { name = "kaleido", specifier = ">=0.2.1" }, { name = "pandas", specifier = ">=3.0.5" }, { name = "plotly", specifier = ">=6.9.0" }, + { name = "velo-sdk", marker = "extra == 'velo'", specifier = "==0.0.20" }, ] +provides-extras = ["velo"] [[package]] name = "crc-framework" @@ -3241,6 +3257,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] +[[package]] +name = "velo-sdk" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff" }, + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/39/289da57496cd872b64cfa4dd6e23bcb570fe3da82e343e299f8e40056281/velo_sdk-0.0.20.tar.gz", hash = "sha256:99bdaa381282d7c7f2a38849bd2971d660f1d70e29379c65beb1928f1261a714", size = 21649, upload-time = "2026-02-26T01:08:59.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/70/0f865e9b533843325b6a61a6cb9fece57712c7504ff549fdd61890557231/velo_sdk-0.0.20-py3-none-any.whl", hash = "sha256:d4b1ee5031de2f8ace53b669f74560d297267ddb82bc491b92dfbdb74d7bd2d5", size = 23737, upload-time = "2026-02-26T01:08:58.743Z" }, +] + [[package]] name = "wcwidth" version = "0.8.2" From 0ecc24634bd81e0ddbcccb21623ba11cd21d9d73 Mon Sep 17 00:00:00 2001 From: Runzhou Li Date: Tue, 18 Aug 2026 15:56:13 -0400 Subject: [PATCH 2/5] Internalize decisions gating within skills, simplify opening prompts --- .../compare-crc-velo-assessments/SKILL.md | 12 +++++++ .../agents/openai.yaml | 2 +- .../crc-assess-asset-portfolio-risk/SKILL.md | 13 +++++++ .../agents/openai.yaml | 2 +- .../crc-model-flood-insurance-loss/SKILL.md | 13 +++++++ .../agents/openai.yaml | 2 +- .../skills/crc-screen-mortgage-flood/SKILL.md | 12 +++++++ .../agents/openai.yaml | 2 +- .../velo-assess-company-climate-risk/SKILL.md | 12 +++++++ .../agents/openai.yaml | 2 +- .../SKILL.md | 11 ++++++ .../agents/openai.yaml | 2 +- .../velo-underwrite-property-climate/SKILL.md | 12 +++++++ .../agents/openai.yaml | 2 +- README.md | 35 +++++++++---------- 15 files changed, 109 insertions(+), 25 deletions(-) diff --git a/.agents/skills/compare-crc-velo-assessments/SKILL.md b/.agents/skills/compare-crc-velo-assessments/SKILL.md index 403103d..18fbd50 100644 --- a/.agents/skills/compare-crc-velo-assessments/SKILL.md +++ b/.agents/skills/compare-crc-velo-assessments/SKILL.md @@ -7,6 +7,18 @@ description: Reconcile an open CRC climate-risk assessment with its VELO/CDT ent Make the open/proprietary relationship credible by preserving both provenance and non-equivalence. +## Guided intake + +Treat `Use $compare-crc-velo-assessments` as a complete invocation. First look +for one CRC result and one VELO/CDT result already identified in the conversation +or attached files. If the pair is unambiguous, proceed without another question. +Otherwise ask the user to identify or attach the two results; if one side does +not exist, recommend the appropriate twin skill. Infer the semantic crosswalk +from metadata and report uncertainty instead of asking a non-expert to specify +alignment fields or comparison classes. Advanced users may provide explicit +artifacts, alignment constraints, or decision criteria, which override inferred +defaults when valid. + ## Demo bootstrap For a zero-credential file-handling smoke test, first run diff --git a/.agents/skills/compare-crc-velo-assessments/agents/openai.yaml b/.agents/skills/compare-crc-velo-assessments/agents/openai.yaml index 5acf9d7..544e546 100644 --- a/.agents/skills/compare-crc-velo-assessments/agents/openai.yaml +++ b/.agents/skills/compare-crc-velo-assessments/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Compare CRC and VELO Assessments" short_description: "Reconcile open and enterprise climate results" - default_prompt: "Use $compare-crc-velo-assessments to compare these CRC and VELO outputs and explain the capability lift." + default_prompt: "Use $compare-crc-velo-assessments." diff --git a/.agents/skills/crc-assess-asset-portfolio-risk/SKILL.md b/.agents/skills/crc-assess-asset-portfolio-risk/SKILL.md index 43fec27..de306e5 100644 --- a/.agents/skills/crc-assess-asset-portfolio-risk/SKILL.md +++ b/.agents/skills/crc-assess-asset-portfolio-risk/SKILL.md @@ -7,6 +7,19 @@ description: Assess a user-supplied portfolio or Overture candidate locations ag Keep per-hazard results auditable and refuse to fabricate a combined portfolio score. +## Guided intake + +Treat `Use $crc-assess-asset-portfolio-risk` as a complete invocation. Reuse a +clear target, portfolio, and decision context; otherwise ask one question: +“Which portfolio or geographic area should I assess, and what decision should +the screen inform?” For a generic geographic demonstration, default to riverine +flood, resolve a narrow AOI, choose EFAS in Europe or GloFAS elsewhere, and +source up to 10 covered Overture candidates at confidence 0.8. Add EDO drought +when the question or geography makes it relevant. Do not ask non-experts for +bounds, H3 resolution, adapters, file paths, raw scenario identifiers, or +Overture categories unless a material ambiguity remains. State inferred choices +before materialization; valid advanced inputs override them. + ## Demo bootstrap When no inputs are supplied, materialize JRC flood through the EFAS/GloFAS diff --git a/.agents/skills/crc-assess-asset-portfolio-risk/agents/openai.yaml b/.agents/skills/crc-assess-asset-portfolio-risk/agents/openai.yaml index 5a9713f..37460cd 100644 --- a/.agents/skills/crc-assess-asset-portfolio-risk/agents/openai.yaml +++ b/.agents/skills/crc-assess-asset-portfolio-risk/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "CRC Asset Portfolio Risk" short_description: "Assess supplied assets with open climate hazards" - default_prompt: "Use $crc-assess-asset-portfolio-risk to assess this asset book with CRC hazard datasets." + default_prompt: "Use $crc-assess-asset-portfolio-risk." diff --git a/.agents/skills/crc-model-flood-insurance-loss/SKILL.md b/.agents/skills/crc-model-flood-insurance-loss/SKILL.md index 79daeee..c0f9dc3 100644 --- a/.agents/skills/crc-model-flood-insurance-loss/SKILL.md +++ b/.agents/skills/crc-model-flood-insurance-loss/SKILL.md @@ -7,6 +7,19 @@ description: Apply an explicit depth-damage function to a CRC canonical flood da Model hazard-to-damage transparently. Do not call the result a premium, technical price, reserve, or regulatory capital requirement. +## Guided intake + +Treat `Use $crc-model-flood-insurance-loss` as a complete invocation. Reuse +clear targets and attachments; otherwise ask one question: “Which portfolio or +area should I assess, and is this an exploratory demonstration or a +decision-facing analysis?” For a demonstration, infer the narrow AOI, JRC +EFAS/GloFAS source, covered Overture candidates, standard return periods, and +the illustrative curve documented below. Do not ask for bounds, paths, source +products, or raw scenario identifiers. For decision-facing work, require an +authorized exposure set and approved curve. Read pathway and horizon from +metadata; if several valid scenarios remain, recommend a concise choice and ask +only then. Never mix scenarios. Valid advanced inputs override defaults. + ## Demo bootstrap When the user supplies no files, materialize canonical JRC EFAS/GloFAS for a diff --git a/.agents/skills/crc-model-flood-insurance-loss/agents/openai.yaml b/.agents/skills/crc-model-flood-insurance-loss/agents/openai.yaml index 7702602..15b9314 100644 --- a/.agents/skills/crc-model-flood-insurance-loss/agents/openai.yaml +++ b/.agents/skills/crc-model-flood-insurance-loss/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "CRC Flood Insurance Loss" short_description: "Model open-data flood damage for insured assets" - default_prompt: "Use $crc-model-flood-insurance-loss to estimate flood damage ratios for this insured portfolio." + default_prompt: "Use $crc-model-flood-insurance-loss." diff --git a/.agents/skills/crc-screen-mortgage-flood/SKILL.md b/.agents/skills/crc-screen-mortgage-flood/SKILL.md index 11d792c..ad1a067 100644 --- a/.agents/skills/crc-screen-mortgage-flood/SKILL.md +++ b/.agents/skills/crc-screen-mortgage-flood/SKILL.md @@ -7,6 +7,18 @@ description: Screen residential or commercial mortgage collateral or Overture ca Produce a screening result, not an automated lending decision. +## Guided intake + +Treat `Use $crc-screen-mortgage-flood` as a complete invocation. Reuse a target +or assets already present in the conversation; otherwise ask one question: +“Which property, city, or region should I screen?” Do not ask a non-expert for +bounds, a JRC product, Overture filters, file paths, or return periods. Resolve a +narrow AOI, choose EFAS within Europe and GloFAS elsewhere, source up to 10 +covered Overture candidates at confidence 0.8, and evaluate 25-, 100-, and +500-year depths. Show these inferred demo choices and the lazy plan before +materialization. User-supplied assets or valid advanced settings override the +defaults. + ## Demo bootstrap When the user names an area but supplies no assets, establish and show narrow diff --git a/.agents/skills/crc-screen-mortgage-flood/agents/openai.yaml b/.agents/skills/crc-screen-mortgage-flood/agents/openai.yaml index 9a28f91..0bf033f 100644 --- a/.agents/skills/crc-screen-mortgage-flood/agents/openai.yaml +++ b/.agents/skills/crc-screen-mortgage-flood/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "CRC Mortgage Flood Screen" short_description: "Screen mortgage collateral with open flood data" - default_prompt: "Use $crc-screen-mortgage-flood to screen this collateral file for open-data flood exposure." + default_prompt: "Use $crc-screen-mortgage-flood." diff --git a/.agents/skills/velo-assess-company-climate-risk/SKILL.md b/.agents/skills/velo-assess-company-climate-risk/SKILL.md index ef8cbf3..d41ff48 100644 --- a/.agents/skills/velo-assess-company-climate-risk/SKILL.md +++ b/.agents/skills/velo-assess-company-climate-risk/SKILL.md @@ -7,6 +7,18 @@ description: Assess company or market-index physical climate risk with VELO/CDT Use VELO's asset and ownership intelligence to answer a business diligence question; preserve scenario and metric definitions. +## Guided intake + +Treat `Use $velo-assess-company-climate-risk` as a complete invocation. Reuse a +clear company, index, and decision context; otherwise ask one question: “Which +company or market index should I assess, and what decision should it inform?” +Resolve identity and supported pathways/horizons through the authorized +connection. Choose only an unambiguous match, recommend a useful scenario +comparison, and ask a concise follow-up only when identity or scenario intent +is materially ambiguous. Do not ask non-experts for company IDs, index IDs, or +raw scenario codes the service can discover. Valid advanced inputs override +defaults. + ## Demo bootstrap This live enterprise skill does not require local asset files. Use the authorized diff --git a/.agents/skills/velo-assess-company-climate-risk/agents/openai.yaml b/.agents/skills/velo-assess-company-climate-risk/agents/openai.yaml index c06b93b..6189f07 100644 --- a/.agents/skills/velo-assess-company-climate-risk/agents/openai.yaml +++ b/.agents/skills/velo-assess-company-climate-risk/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "VELO Company Climate Risk" short_description: "Assess company and market climate risk in VELO" - default_prompt: "Use $velo-assess-company-climate-risk to perform climate diligence on this company or market index." + default_prompt: "Use $velo-assess-company-climate-risk." diff --git a/.agents/skills/velo-triage-portfolio-insurability/SKILL.md b/.agents/skills/velo-triage-portfolio-insurability/SKILL.md index f190829..2fe51c7 100644 --- a/.agents/skills/velo-triage-portfolio-insurability/SKILL.md +++ b/.agents/skills/velo-triage-portfolio-insurability/SKILL.md @@ -7,6 +7,17 @@ description: Triage a company's physical-asset portfolio using VELO's uninsurabl Use VELO classifications as platform signals, not as universal actuarial or underwriting policy. +## Guided intake + +Treat `Use $velo-triage-portfolio-insurability` as a complete invocation. Reuse +a clear company from the conversation; otherwise ask only for the company name +or VELO company ID. Resolve candidates and supported pathways/horizons through +the authorized connection. Select an unambiguous company, show a short list when +identity is ambiguous, and recommend a relevant scenario or comparison rather +than asking the user for raw platform codes. Ask a follow-up only when identity, +scenario intent, or decision context is materially unclear. Valid advanced +inputs override defaults. + ## Demo bootstrap This live enterprise skill does not require local portfolio files. Use the diff --git a/.agents/skills/velo-triage-portfolio-insurability/agents/openai.yaml b/.agents/skills/velo-triage-portfolio-insurability/agents/openai.yaml index 41c068a..5dc771d 100644 --- a/.agents/skills/velo-triage-portfolio-insurability/agents/openai.yaml +++ b/.agents/skills/velo-triage-portfolio-insurability/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "VELO Portfolio Insurability Triage" short_description: "Triage uninsurable and stranded portfolio assets" - default_prompt: "Use $velo-triage-portfolio-insurability to identify climate-driven insurability concerns in this portfolio." + default_prompt: "Use $velo-triage-portfolio-insurability." diff --git a/.agents/skills/velo-underwrite-property-climate/SKILL.md b/.agents/skills/velo-underwrite-property-climate/SKILL.md index 7b19ec9..37421d5 100644 --- a/.agents/skills/velo-underwrite-property-climate/SKILL.md +++ b/.agents/skills/velo-underwrite-property-climate/SKILL.md @@ -7,6 +7,18 @@ description: Perform a higher-calibre property climate underwriting assessment t Use enterprise data to deepen a CRC baseline; do not turn a platform score into an automatic lending decision. +## Guided intake + +Treat `Use $velo-underwrite-property-climate` as a complete invocation. Reuse a +clear property from the conversation; otherwise ask only for a property name, +address, or VELO asset ID. Search through the authorized connection, select +automatically only when identity is unambiguous, and show a short candidate list +when it is not. Discover supported pathways and horizons internally. Recommend +a decision-relevant scenario or comparison and ask a concise follow-up only when +the choice would materially change the assessment. Do not ask for API IDs or raw +scenario codes that the service can discover. Valid advanced inputs override +defaults. + ## Demo bootstrap This live enterprise skill does not require a local CSV or Parquet. If the user diff --git a/.agents/skills/velo-underwrite-property-climate/agents/openai.yaml b/.agents/skills/velo-underwrite-property-climate/agents/openai.yaml index 1598610..8e23d24 100644 --- a/.agents/skills/velo-underwrite-property-climate/agents/openai.yaml +++ b/.agents/skills/velo-underwrite-property-climate/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "VELO Property Climate Underwriting" short_description: "Underwrite property with enterprise climate data" - default_prompt: "Use $velo-underwrite-property-climate to assess this property across VELO climate scenarios." + default_prompt: "Use $velo-underwrite-property-climate." diff --git a/README.md b/README.md index 2019275..19c264a 100644 --- a/README.md +++ b/README.md @@ -9,40 +9,39 @@ Each open CRC baseline has a higher-calibre VELO/CDT enterprise twin. ## Start here: copy a prompt - Link [`RiskThinking/crc-docs`](https://github.com/RiskThinking/crc-docs) into an AI tool that can reference repository files -- Copy a prompt from the table below -- Replace `{placeholders}` for desired targets (e.g. `{city}, {country}` to `Toronto, Canada`) +- Start with only the skill name; the AI will ask for the minimum information it needs +- Optionally include the target or advanced assumptions you already know | Decision | Open CRC baseline | VELO/CDT enterprise twin | |---|---|---| -| **Mortgage / collateral** | Use `$crc-screen-mortgage-flood` for `{city}, {country}`. Resolve and show narrow WGS84 bounds. Choose JRC EFAS for Europe or GloFAS elsewhere, show the lazy plan, and ask before materializing it. Then source up to `{10}` relevant candidates from the latest Overture Places release, filtered to JRC source-geometry coverage. Report category, confidence, release, and that these are not verified collateral. Evaluate 25-, 100-, and 500-year flood depths; do not make a lending decision. | Use `$velo-underwrite-property-climate` for `{property name or VELO asset ID}` in `{city}, {country}`. Discover valid pathways and horizons, show ambiguous matches for selection, then assess `{pathway}` at `{horizon}`. Report identity evidence, multi-factor drivers, tail-risk metrics, and what the enterprise result adds beyond the JRC screen. | -| **Insurance** | Use `$crc-model-flood-insurance-loss` for open candidate locations in `{city}, {country}`. Materialize canonical JRC flood data, then source Overture candidates for the same bounds filtered to modeled JRC coverage. Read pathway and horizon from the metadata. Use our approved `{depth/damage knots}`; if none are supplied, use the repository's illustrative knots and label the result non-production. Preserve event-aligned return periods and state that Overture candidates are not an insured portfolio. | Use `$velo-triage-portfolio-insurability` for `{company or company ID}` under `{pathway}` at `{horizon}`. Resolve supported values first. List VELO-classified uninsurable and stranded assets, overlap, countries, asset types, and coverage exceptions. Keep platform classifications separate from underwriting policy. | -| **Corporate finance / investment** | Use `$crc-assess-asset-portfolio-risk` for `{city/region}`. Resolve and show bounds, then materialize applicable JRC hazards: EFAS/GloFAS flood and, when relevant, EDO drought. Source Overture candidates inside hazard coverage using categories relevant to `{business question}`; disclose that ownership and materiality are unverified. Keep hazards separate when horizons or semantics differ and do not fabricate a combined score. | Use `$velo-assess-company-climate-risk` for `{company}` or market index `{index}`. Show matching candidates before selection. Compare `{pathway A}` with `{pathway B}` at `{horizon}`, then report total score, factor attribution, country concentration, asset-type concentration, coverage exceptions, and the capability lift over the Overture/JRC baseline. | +| **Mortgage / collateral** | **Start:** `Use $crc-screen-mortgage-flood.`
**Optional:** `Use $crc-screen-mortgage-flood for Toronto, Canada.` | **Start:** `Use $velo-underwrite-property-climate.`
**Optional:** `Use $velo-underwrite-property-climate for 392 Markham Street, Toronto.` | +| **Insurance** | **Start:** `Use $crc-model-flood-insurance-loss.`
**Optional:** `Use $crc-model-flood-insurance-loss for Rotterdam using my attached portfolio and approved depth-damage curve.` | **Start:** `Use $velo-triage-portfolio-insurability.`
**Optional:** `Use $velo-triage-portfolio-insurability for Example Insurance Holdings.` | +| **Corporate finance / investment** | **Start:** `Use $crc-assess-asset-portfolio-risk.`
**Optional:** `Use $crc-assess-asset-portfolio-risk for Frankfurt, with flood and drought.` | **Start:** `Use $velo-assess-company-climate-risk.`
**Optional:** `Use $velo-assess-company-climate-risk for the S&P 500.` | After running both sides for the same real target: ```text -Use $compare-crc-velo-assessments on {crc-output.parquet} and -{velo-output.json}. Align asset/entity, hazard/factor, pathway, horizon, metric, -unit, spatial precision, and source version. Classify each comparison as direct, -directional, capability-lift-only, or not comparable. Explain whether the added -enterprise evidence changes the decision and record reproducible gaps. +Use $compare-crc-velo-assessments. ``` +The skill will use the paired outputs already in the conversation when they are +unambiguous. Advanced users can attach or name the two artifacts explicitly. + If you want the AI to choose for you, begin with: ```text -Inspect .agents/skills/ in this repository. For my -{mortgage / insurance / investment} question, recommend an open CRC skill and -its VELO/CDT twin, explain the evidence each can provide, and list required -approvals or credentials. Do not materialize data yet. +Which CRC/VELO skill should I use for my climate-risk question? ``` ## Peel down into the playbooks -The prompt gives the AI the decision and target. The selected `SKILL.md` supplies -the workflow, guardrails, scripts, and deeper domain reference. An agent should -load the selected pair—not every skill in the repository—and add the comparison -skill only after both outputs exist. +A bare skill invocation starts a guided intake. The selected `SKILL.md` owns the +workflow, guardrails, scripts, and safe defaults—including source, bounds, +scenario discovery, and demo configuration—so non-experts are not asked for +technical parameters the AI can determine. Any values supplied by an advanced +user override those defaults when valid. An agent should load the selected +pair—not every skill in the repository—and add the comparison skill only after +both outputs exist. | Decision | Load these playbooks | |---|---| From af2199c6ada97415f4d3c065c555cc7f394f8ba0 Mon Sep 17 00:00:00 2001 From: Runzhou Li Date: Tue, 18 Aug 2026 16:12:10 -0400 Subject: [PATCH 3/5] Weave in CDT Express MCP server as a prerequisite for VELO/CDT prompts --- .../velo-assess-company-climate-risk/SKILL.md | 16 ++++++++-- .../agents/openai.yaml | 8 +++++ .../references/playbook.md | 32 +++++++++++++++++++ .../SKILL.md | 17 ++++++++-- .../agents/openai.yaml | 8 +++++ .../references/playbook.md | 23 +++++++++++++ .../velo-underwrite-property-climate/SKILL.md | 17 ++++++++-- .../agents/openai.yaml | 8 +++++ .../references/playbook.md | 24 ++++++++++++++ README.md | 14 +++++--- 10 files changed, 157 insertions(+), 10 deletions(-) diff --git a/.agents/skills/velo-assess-company-climate-risk/SKILL.md b/.agents/skills/velo-assess-company-climate-risk/SKILL.md index d41ff48..01d04c4 100644 --- a/.agents/skills/velo-assess-company-climate-risk/SKILL.md +++ b/.agents/skills/velo-assess-company-climate-risk/SKILL.md @@ -1,6 +1,6 @@ --- name: velo-assess-company-climate-risk -description: Assess company or market-index physical climate risk with VELO/CDT using company, asset, market, climate-score, impact, and aggregation APIs. Use for investment diligence, lending portfolios, company benchmarking, country or asset-type concentration, scenario comparison, or the enterprise twin to a CRC supplied-asset assessment. +description: Assess company or market-index physical climate risk with VELO/CDT using the remote CDT Express MCP server or velo-sdk company, asset, market, climate-score, impact, and aggregation APIs. Use for investment diligence, lending portfolios, company benchmarking, country or asset-type concentration, scenario comparison, or the enterprise twin to a CRC supplied-asset assessment. --- # VELO company climate risk @@ -19,10 +19,22 @@ is materially ambiguous. Do not ask non-experts for company IDs, index IDs, or raw scenario codes the service can discover. Valid advanced inputs override defaults. +## CDT Express MCP execution + +Prefer the declared `CDT Express` remote MCP dependency for an AI chat session. +If it is not connected, direct the user to add +`https://mcp.riskthinking.ai/mcp` and complete VELO OAuth; never request an API +key in chat. Inspect the connected tool list and schemas before calls. Use the +company or market search/detail tools to resolve the target, then the matching +climate-score and constituent/asset-score tools. Read +[references/playbook.md](references/playbook.md) for exact sequences. Prefer +bounded, sorted calls for decision-relevant concentrations; confirm scope before +exhaustive retrieval. + ## Demo bootstrap This live enterprise skill does not require local asset files. Use the authorized -MCP or SDK connection to search the requested company or index and discover +MCP connection, or the SDK fallback, to search the requested company or index and discover supported pathways and horizons, then ask the user to resolve ambiguity. If no authorized connection is available, explain the prerequisite and stop. The repository's synthetic VELO-shaped JSON is only for reconciliation smoke tests, diff --git a/.agents/skills/velo-assess-company-climate-risk/agents/openai.yaml b/.agents/skills/velo-assess-company-climate-risk/agents/openai.yaml index 6189f07..c3e7bff 100644 --- a/.agents/skills/velo-assess-company-climate-risk/agents/openai.yaml +++ b/.agents/skills/velo-assess-company-climate-risk/agents/openai.yaml @@ -2,3 +2,11 @@ interface: display_name: "VELO Company Climate Risk" short_description: "Assess company and market climate risk in VELO" default_prompt: "Use $velo-assess-company-climate-risk." + +dependencies: + tools: + - type: "mcp" + value: "io.github.RiskThinking/cdt-express-mcp" + description: "CDT Express climate, asset, company, and market tools" + transport: "streamable_http" + url: "https://mcp.riskthinking.ai/mcp" diff --git a/.agents/skills/velo-assess-company-climate-risk/references/playbook.md b/.agents/skills/velo-assess-company-climate-risk/references/playbook.md index cc2cf4b..c9112ff 100644 --- a/.agents/skills/velo-assess-company-climate-risk/references/playbook.md +++ b/.agents/skills/velo-assess-company-climate-risk/references/playbook.md @@ -1,5 +1,37 @@ # Company and market diligence playbook +## Remote CDT Express MCP route + +Prefer the OAuth-connected server at `https://mcp.riskthinking.ai/mcp`. Inspect +its current tool schemas before choosing scenario values or fields. + +For a company: + +1. Resolve identity with `search_companies` and `get_company`. +2. Retrieve the total with `get_company_climate_scores`. +3. Retrieve owned assets with `get_company_assets` or, only when justified, + `get_all_company_assets`. +4. Use `get_asset_climate_scores` on a bounded decision-relevant asset set for + drivers and concentrations. Preserve excluded and failed assets as coverage + exceptions. + +For a market index: + +1. Resolve it with `search_market_indexes` and `get_market_index`. +2. Retrieve the total with `get_market_index_climate_scores`. +3. Use `get_market_index_companies_climate_scores` and + `get_market_index_assets_climate_scores` with sorting and limits to identify + concentrations; use their `get_all_...` counterparts only for a justified + exhaustive export. + +Use `get_metrics_definition` when explaining unfamiliar score fields. Complete +VELO OAuth and never request `CDT_API_KEY` in chat. If a needed factor, +aggregation, or high-risk classification is not exposed by the connected MCP +version, state the gap and use the SDK route only when locally configured and +authorized. + +## Local SDK fallback + In `crc-docs`, install the optional client with `uv sync --extra velo`, then run the bundled script through `uv run python`. ## Company method map (`velo-sdk==0.0.20`) diff --git a/.agents/skills/velo-triage-portfolio-insurability/SKILL.md b/.agents/skills/velo-triage-portfolio-insurability/SKILL.md index 2fe51c7..bb90f1c 100644 --- a/.agents/skills/velo-triage-portfolio-insurability/SKILL.md +++ b/.agents/skills/velo-triage-portfolio-insurability/SKILL.md @@ -1,6 +1,6 @@ --- name: velo-triage-portfolio-insurability -description: Triage a company's physical-asset portfolio using VELO's uninsurable and stranded asset classifications under a selected pathway and horizon. Use for insurance portfolio review, renewal triage, climate concentration, high-risk asset lists, or the higher-calibre twin to a CRC flood damage assessment. +description: Triage a company's physical-asset portfolio through the remote CDT Express MCP server or velo-sdk using VELO climate scores and documented uninsurable and stranded thresholds. Use for insurance portfolio review, renewal triage, climate concentration, high-risk asset lists, or the higher-calibre twin to a CRC flood damage assessment. --- # VELO portfolio insurability triage @@ -18,10 +18,23 @@ than asking the user for raw platform codes. Ask a follow-up only when identity, scenario intent, or decision context is materially unclear. Valid advanced inputs override defaults. +## CDT Express MCP execution + +Prefer the declared `CDT Express` remote MCP dependency for an AI chat session. +If it is not connected, direct the user to add +`https://mcp.riskthinking.ai/mcp` and complete VELO OAuth; never request an API +key in chat. Inspect the connected tool list and schemas before calls. Resolve +the company with `search_companies` and `get_company`, obtain its assets with +`get_company_assets` or `get_all_company_assets`, then retrieve supported +asset-level scores with `get_asset_climate_scores`. Read +[references/playbook.md](references/playbook.md) before applying platform +thresholds. Bound or confirm a large exhaustive run rather than silently making +unbounded tool calls. + ## Demo bootstrap This live enterprise skill does not require local portfolio files. Use the -authorized MCP or SDK connection to resolve the requested company and discover +authorized MCP connection, or the SDK fallback, to resolve the requested company and discover supported pathways and horizons. Ask the user to select among ambiguous matches. If no authorized connection is available, explain the prerequisite and stop; never present mocked classifications as live VELO results. diff --git a/.agents/skills/velo-triage-portfolio-insurability/agents/openai.yaml b/.agents/skills/velo-triage-portfolio-insurability/agents/openai.yaml index 5dc771d..378df03 100644 --- a/.agents/skills/velo-triage-portfolio-insurability/agents/openai.yaml +++ b/.agents/skills/velo-triage-portfolio-insurability/agents/openai.yaml @@ -2,3 +2,11 @@ interface: display_name: "VELO Portfolio Insurability Triage" short_description: "Triage uninsurable and stranded portfolio assets" default_prompt: "Use $velo-triage-portfolio-insurability." + +dependencies: + tools: + - type: "mcp" + value: "io.github.RiskThinking/cdt-express-mcp" + description: "CDT Express climate, asset, company, and market tools" + transport: "streamable_http" + url: "https://mcp.riskthinking.ai/mcp" diff --git a/.agents/skills/velo-triage-portfolio-insurability/references/playbook.md b/.agents/skills/velo-triage-portfolio-insurability/references/playbook.md index 1694a33..a656f09 100644 --- a/.agents/skills/velo-triage-portfolio-insurability/references/playbook.md +++ b/.agents/skills/velo-triage-portfolio-insurability/references/playbook.md @@ -1,5 +1,28 @@ # Insurability triage playbook +## Remote CDT Express MCP route + +Prefer the OAuth-connected server at `https://mcp.riskthinking.ai/mcp` for a +chat-native run: + +1. Inspect the available tools and schemas, then resolve the company with + `search_companies` and `get_company`. +2. Obtain the company's assets with `get_company_assets`; use + `get_all_company_assets` only when an exhaustive run is appropriate. +3. Call `get_asset_climate_scores` for each in-scope asset under one supported + pathway and horizon. Confirm scope before a large fan-out. +4. Apply the documented VELO thresholds below to returned `cvar_95` values; + preserve nulls and failed calls as coverage exceptions. +5. Use `get_company_climate_scores` for portfolio context, not as a substitute + for asset classification. + +Complete the server's VELO OAuth flow and never request `CDT_API_KEY` in chat. +If the connected MCP version cannot return the required asset metric or +portfolio scope, report the gap and use the SDK route only when locally +configured and authorized. + +## Local SDK fallback + In `crc-docs`, install the optional client with `uv sync --extra velo`, then run the bundled script through `uv run python`. In `velo-sdk==0.0.20`, company helpers describe: diff --git a/.agents/skills/velo-underwrite-property-climate/SKILL.md b/.agents/skills/velo-underwrite-property-climate/SKILL.md index 37421d5..6a11c47 100644 --- a/.agents/skills/velo-underwrite-property-climate/SKILL.md +++ b/.agents/skills/velo-underwrite-property-climate/SKILL.md @@ -1,6 +1,6 @@ --- name: velo-underwrite-property-climate -description: Perform a higher-calibre property climate underwriting assessment through VELO/CDT using velo-sdk or an equivalent authorized MCP surface. Use for a named property or VELO asset when the user needs proprietary asset resolution, multi-factor climate impacts, future pathways and horizons, VaR/CVaR, or an enterprise twin to a CRC mortgage flood screen. +description: Perform a higher-calibre property climate underwriting assessment through VELO/CDT using the remote CDT Express MCP server or velo-sdk. Use for a named property or VELO asset when the user needs proprietary asset resolution, multi-factor climate impacts, future pathways and horizons, VaR/CVaR, or an enterprise twin to a CRC mortgage flood screen. --- # VELO property climate underwriting @@ -19,10 +19,23 @@ the choice would materially change the assessment. Do not ask for API IDs or raw scenario codes that the service can discover. Valid advanced inputs override defaults. +## CDT Express MCP execution + +Prefer the declared `CDT Express` remote MCP dependency for an AI chat session. +If it is not connected, direct the user to add +`https://mcp.riskthinking.ai/mcp` and complete VELO OAuth; never request an API +key in chat. Inspect the connected tool list and input schemas before calls. +Use `search_assets`, `get_asset`, and `get_asset_climate_scores`; use the climate +metric and distribution tools only when their schemas support the requested +factor-level evidence. Read [references/playbook.md](references/playbook.md) for +the current MCP sequence and SDK fallback. Do not invent missing tools or +fields. + ## Demo bootstrap This live enterprise skill does not require a local CSV or Parquet. If the user -has not supplied an asset ID, use the authorized MCP or SDK connection to search +has not supplied an asset ID, use the authorized MCP connection, or the SDK +fallback, to search their property query, show candidates, and require a selection when ambiguous. If no authorized connection is available, explain the prerequisite and stop; never replace enterprise evidence with synthetic data. diff --git a/.agents/skills/velo-underwrite-property-climate/agents/openai.yaml b/.agents/skills/velo-underwrite-property-climate/agents/openai.yaml index 8e23d24..819a55d 100644 --- a/.agents/skills/velo-underwrite-property-climate/agents/openai.yaml +++ b/.agents/skills/velo-underwrite-property-climate/agents/openai.yaml @@ -2,3 +2,11 @@ interface: display_name: "VELO Property Climate Underwriting" short_description: "Underwrite property with enterprise climate data" default_prompt: "Use $velo-underwrite-property-climate." + +dependencies: + tools: + - type: "mcp" + value: "io.github.RiskThinking/cdt-express-mcp" + description: "CDT Express climate, asset, company, and market tools" + transport: "streamable_http" + url: "https://mcp.riskthinking.ai/mcp" diff --git a/.agents/skills/velo-underwrite-property-climate/references/playbook.md b/.agents/skills/velo-underwrite-property-climate/references/playbook.md index a13d63a..4ac1dae 100644 --- a/.agents/skills/velo-underwrite-property-climate/references/playbook.md +++ b/.agents/skills/velo-underwrite-property-climate/references/playbook.md @@ -1,5 +1,29 @@ # Property underwriting playbook +## Remote CDT Express MCP route + +Prefer the OAuth-connected server at `https://mcp.riskthinking.ai/mcp` for a +chat-native run: + +1. Inspect the available tools and their schemas; supported scenario values are + defined there and can change by server version. +2. Call `search_assets` for a name/address, or `get_asset` for a known ID. +3. Call `get_asset_climate_scores` for the chosen pathway and horizon. +4. When location-level factor evidence is needed and supported, use + `get_climate_metrics_exposure`, `get_climate_metrics_impact`, + `get_climate_metrics_probability_adjusted_impact`, or the corresponding + distribution tool. Use `get_metrics_definition` before explaining unfamiliar + metrics. +5. Preserve tool names, inputs, returned metadata, nulls, and errors in the + evidence memo. Never equate a location metric with an asset score without a + documented semantic match. + +Complete the server's VELO OAuth flow. Never request or place `CDT_API_KEY` in a +prompt. If the connected server does not expose a required capability, state the +gap and use the SDK route below only when locally configured and authorized. + +## Local SDK fallback + In `crc-docs`, install the optional client with `uv sync --extra velo`, then run the bundled script through `uv run python`. ## Read-only route in velo-sdk 0.0.20 diff --git a/README.md b/README.md index 19c264a..998d07a 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Each open CRC baseline has a higher-calibre VELO/CDT enterprise twin. - Start with only the skill name; the AI will ask for the minimum information it needs - Optionally include the target or advanced assumptions you already know +Note: to run an enterprise twin, connect the [`CDT Express MCP server`](https://github.com/RiskThinking/cdt-express-mcp) in your AI tool using `https://mcp.riskthinking.ai/mcp`, complete the VELO OAuth flow, and enable its tools for the chat. Do not paste an API key into the conversation. Each VELO/CDT skill declares this remote server as a dependency, discovers its supported tool schemas, and guides the relevant read-only calls. + | Decision | Open CRC baseline | VELO/CDT enterprise twin | |---|---|---| | **Mortgage / collateral** | **Start:** `Use $crc-screen-mortgage-flood.`
**Optional:** `Use $crc-screen-mortgage-flood for Toronto, Canada.` | **Start:** `Use $velo-underwrite-property-climate.`
**Optional:** `Use $velo-underwrite-property-climate for 392 Markham Street, Toronto.` | @@ -67,10 +69,14 @@ For an open baseline without supplied files, the AI should: 5. Read scenario, units, support, and source version from canonical JRC metadata before evaluation; never guess or silently combine them. -The VELO/CDT twins do not need local CSV or Parquet inputs. They require an -authorized MCP connection or `velo-sdk` configuration, then discover companies, -indices, assets, pathways, and horizons from the connected service. Never paste -`RISKTHINKING_API_KEY` into a chat or substitute synthetic JSON for live evidence. +The VELO/CDT twins do not need local CSV or Parquet inputs. Prefer the remote +CDT Express MCP server at `https://mcp.riskthinking.ai/mcp`; its OAuth flow +authorizes access without exposing a CDT key to the AI tool. The skills discover +companies, indices, assets, pathways, and horizons from available tool schemas +and responses. `velo-sdk` remains a local fallback for capabilities not exposed +by the connected MCP version. Never paste `CDT_API_KEY` or +`RISKTHINKING_API_KEY` into a chat or substitute synthetic JSON for live +evidence. ## Run the open demonstration From b433c9b4c2c33100574ea99dd1c16b487e6164d4 Mon Sep 17 00:00:00 2001 From: Runzhou Li Date: Tue, 18 Aug 2026 16:18:01 -0400 Subject: [PATCH 4/5] Add practical visualizations into the skills --- .../compare-crc-velo-assessments/SKILL.md | 18 +++++++++++ .../crc-assess-asset-portfolio-risk/SKILL.md | 18 +++++++++++ .../crc-model-flood-insurance-loss/SKILL.md | 17 ++++++++++ .../skills/crc-screen-mortgage-flood/SKILL.md | 26 ++++++++++++++-- .../references/playbook.md | 2 ++ .../scripts/screen_mortgage_flood.py | 2 +- .../velo-assess-company-climate-risk/SKILL.md | 31 +++++++++++++++++-- .../references/playbook.md | 15 +++++---- .../SKILL.md | 17 ++++++++++ .../velo-underwrite-property-climate/SKILL.md | 17 ++++++++++ README.md | 13 ++++++++ .../examples/dogfood/verify_skills.py | 18 ++++++++--- ai-playbooks/examples/run-open-demo.sh | 2 +- pipelines/jrc_flood_pipeline.py | 22 +++++++------ 14 files changed, 191 insertions(+), 27 deletions(-) diff --git a/.agents/skills/compare-crc-velo-assessments/SKILL.md b/.agents/skills/compare-crc-velo-assessments/SKILL.md index 18fbd50..985e8e8 100644 --- a/.agents/skills/compare-crc-velo-assessments/SKILL.md +++ b/.agents/skills/compare-crc-velo-assessments/SKILL.md @@ -42,6 +42,24 @@ the same real target, with aligned dimensions documented below. 5. Produce a paired decision memo: what CRC establishes, what VELO/CDT adds, whether the decision changes, and what evidence remains missing. 6. Add newly observed workflow issues to `ai-playbooks/docs/gap-backlog.md` with a reproducible example and suggested acceptance test, assigned to the CRC or VELO/CDT side. +## Deliverable bundle + +Retain both untouched source artifacts and produce a machine-readable alignment +table containing the crosswalk fields, comparison class, differences, and +coverage notes. Export it as CSV and Parquet or JSON, create matched-asset +GeoJSON when both sides contain compatible locations, and write a self-contained +HTML decision report with a provenance manifest. In the response, show a compact +table of the most decision-relevant aligned and non-aligned findings. Include: + +- side-by-side metric/coverage charts and a comparison-class count chart; and +- a matched-asset map showing only direct or directional spatial comparisons, + with unmatched and capability-lift-only evidence visibly distinct. + +Never plot unlike values on a shared quantitative scale. Keep all reconciliation +rows downloadable, disclose any visible top-N selection, and omit the map with a +clear explanation when spatial identity cannot be aligned rather than forcing a +misleading visualization. + ## Messaging guardrails - Do not position CRC as intentionally crippled. Position it as transparent, extensible infrastructure and reproducible baseline. diff --git a/.agents/skills/crc-assess-asset-portfolio-risk/SKILL.md b/.agents/skills/crc-assess-asset-portfolio-risk/SKILL.md index de306e5..01f522e 100644 --- a/.agents/skills/crc-assess-asset-portfolio-risk/SKILL.md +++ b/.agents/skills/crc-assess-asset-portfolio-risk/SKILL.md @@ -42,6 +42,24 @@ membership. 6. Use `notebooks/multi_scenario_portfolio.ipynb` only as a transparent sensitivity-stress example, not a calibrated projection. Use `crc-framework` risk aggregation only when binary outcomes, dependencies/independence assumptions, confidence levels, and branch limits are explicitly defined. Read [references/playbook.md](references/playbook.md). 7. Pair with `$velo-assess-company-climate-risk` when entity resolution, ownership, benchmarks, more hazards, proprietary data, or enterprise scenario/risk scores matter. +## Deliverable bundle + +Retain one canonical evaluation Parquet per hazard; never replace these with a +presentation-only aggregate. Also create a joined analysis-ready CSV, GeoJSON, +and self-contained HTML report plus a manifest describing every source, +scenario, unit, and non-equivalence. In the response, show a compact table with +asset, hazard, match status, selected metric, unit, pathway, and horizon, sorted +within—not across—incompatible hazards. Include: + +- a portfolio map with a hazard selector or small multiples, marking missing + and extrapolated assets explicitly; and +- per-hazard exposure/concentration charts, with separate panels when units, + horizons, or semantics differ. + +Make the full table and visuals downloadable when supported. Use a bounded, +clearly disclosed top-risk subset only for readability; keep all rows in the +artifacts. Do not fabricate a combined score or silently geocode missing assets. + ## Future dataset seam When the open CDT hazard dataset is released, require it to materialize the current CRC canonical hazard contract. Downstream evaluation must remain unchanged. Until then, label the source as planned and never synthesize its data. diff --git a/.agents/skills/crc-model-flood-insurance-loss/SKILL.md b/.agents/skills/crc-model-flood-insurance-loss/SKILL.md index c0f9dc3..7153a71 100644 --- a/.agents/skills/crc-model-flood-insurance-loss/SKILL.md +++ b/.agents/skills/crc-model-flood-insurance-loss/SKILL.md @@ -44,3 +44,20 @@ analysis requires authorized exposures and an applicable, approved curve. ## Required output Include the exact curve knots/version/source, event-aligned semantics, asset coverage, damage ratios by return period, concentration observations, uncertainty/limitations, and recommended calibration or engineering review. + +## Deliverable bundle + +Retain the event-aligned loss Parquet and its canonical hazard input. Also +create an analysis-ready CSV, asset GeoJSON, curve-knots CSV, and a +self-contained HTML report with a manifest recording scenario, units, curve, +and coverage. In the response, show a compact table sorted by the largest +damage ratio with asset, match status, flood depth, damage ratio, and return +period. Include: + +- a map coloured by damage ratio for one explicitly labelled return period; +- the depth-damage curve with observed asset depths overlaid; and +- a return-period damage-ratio chart for the most exposed assets. + +Keep ratios and any derived currency values visibly separate. Make the table +and visuals downloadable when supported. Do not map confidential exposure +attributes unless authorized, and do not invent coordinates for missing assets. diff --git a/.agents/skills/crc-screen-mortgage-flood/SKILL.md b/.agents/skills/crc-screen-mortgage-flood/SKILL.md index ad1a067..cea921a 100644 --- a/.agents/skills/crc-screen-mortgage-flood/SKILL.md +++ b/.agents/skills/crc-screen-mortgage-flood/SKILL.md @@ -15,9 +15,10 @@ or assets already present in the conversation; otherwise ask one question: bounds, a JRC product, Overture filters, file paths, or return periods. Resolve a narrow AOI, choose EFAS within Europe and GloFAS elsewhere, source up to 10 covered Overture candidates at confidence 0.8, and evaluate 25-, 100-, and -500-year depths. Show these inferred demo choices and the lazy plan before -materialization. User-supplied assets or valid advanced settings override the -defaults. +500-year depths. Use the repository's asset-level flood default of H3 resolution +9 unless a valid advanced setting overrides it. Show these inferred demo choices +and the lazy plan before materialization. User-supplied assets or valid advanced +settings override the defaults. ## Demo bootstrap @@ -45,6 +46,25 @@ verified collateral, ownership, occupancy, value, or insurance exposure. Report scope, source/version, assets submitted/matched/unmatched, units, requested return periods, exposure observations, spatial precision, extrapolation, assumptions, and next action. Separate facts from policy judgments. +## Deliverable bundle + +Retain the canonical hazard and asset-level Parquet output as the source of +truth. Also create an analysis-ready CSV, a GeoJSON of matched and unmatched +locations, and a self-contained HTML report; preserve metadata and warnings in +a manifest. In the response, show a compact table sorted by the highest +screening depth with asset, match status, spatial precision, and 25-, 100-, and +500-year depths including units. Include: + +- a map using distinct symbols for matched, unmatched, and extrapolated assets, + coloured by a clearly named return-period depth; and +- a return-period depth chart for the most exposed assets, without implying a + continuous forecast between events. + +Make the table, chart, and map downloadable separately when the host supports +artifacts. Never place unverified Overture ownership or confidential collateral +details in labels. If coordinates are unavailable, do not geocode silently; +state why the map could not be produced. + ## Boundaries - Target `crc-sdk==0.3.0` and `crc-framework>=0.2,<0.3`. diff --git a/.agents/skills/crc-screen-mortgage-flood/references/playbook.md b/.agents/skills/crc-screen-mortgage-flood/references/playbook.md index 490c796..3637133 100644 --- a/.agents/skills/crc-screen-mortgage-flood/references/playbook.md +++ b/.agents/skills/crc-screen-mortgage-flood/references/playbook.md @@ -18,6 +18,8 @@ Install `crc-sdk[geometry,raster]==0.3.0`. Point-to-H3 conversion needs the geom Overture provenance fields only when appropriate for the output. - AOI bounds: `min_lon min_lat max_lon max_lat` in WGS84. - Source: EFAS for Europe; GloFAS elsewhere. +- H3 resolution: default to 9 for asset-level flood acquisition, matching + `pipelines/jrc_flood_pipeline.py`; record any explicit override in outputs. - Return periods: choose decision-relevant periods within the resolved source support where possible. ## Interpretation diff --git a/.agents/skills/crc-screen-mortgage-flood/scripts/screen_mortgage_flood.py b/.agents/skills/crc-screen-mortgage-flood/scripts/screen_mortgage_flood.py index 4b64ec6..9f9c797 100644 --- a/.agents/skills/crc-screen-mortgage-flood/scripts/screen_mortgage_flood.py +++ b/.agents/skills/crc-screen-mortgage-flood/scripts/screen_mortgage_flood.py @@ -24,7 +24,7 @@ def parser() -> argparse.ArgumentParser: help="Optional path for the materialized canonical JRC hazard dataset", ) p.add_argument("--periods", nargs="+", type=float, default=[25, 100, 500]) - p.add_argument("--h3-resolution", type=int, default=10) + p.add_argument("--h3-resolution", type=int, default=9) p.add_argument("--cache-mode", choices=("reuse", "offline", "refresh", "stream"), default="reuse") p.add_argument("--workers", type=int, default=1, help="Explicit process count; raise only after profiling") p.add_argument("--plan-only", action="store_true", help="Explain without fetching or writing") diff --git a/.agents/skills/velo-assess-company-climate-risk/SKILL.md b/.agents/skills/velo-assess-company-climate-risk/SKILL.md index 01d04c4..7d7b228 100644 --- a/.agents/skills/velo-assess-company-climate-risk/SKILL.md +++ b/.agents/skills/velo-assess-company-climate-risk/SKILL.md @@ -29,7 +29,9 @@ company or market search/detail tools to resolve the target, then the matching climate-score and constituent/asset-score tools. Read [references/playbook.md](references/playbook.md) for exact sequences. Prefer bounded, sorted calls for decision-relevant concentrations; confirm scope before -exhaustive retrieval. +exhaustive retrieval. Treat climate-score tools as risk metrics, not factor +attribution. Report drivers only when an explicit impact/factor-attribution tool +or SDK response supplies them. ## Demo bootstrap @@ -45,10 +47,33 @@ not a substitute for this assessment. 1. Resolve the company or market index. Show ambiguity and require selection rather than choosing silently. 2. Discover supported pathways and horizons. 3. Run `scripts/assess_company.py` with either `--company-id` or `--index-id`. Follow the method map in [references/playbook.md](references/playbook.md) when using equivalent MCP tools. -4. Collect total climate score, factor impacts/attribution, country concentration, asset-type concentration, and high-risk assets where authorized. +4. Collect total climate score, country concentration, asset-type concentration, and high-risk assets where authorized. Collect factor impacts/attribution only from an explicit impact or attribution response; if the active MCP surface has only climate scores, mark drivers unavailable. 5. Compare pathways/horizons as separate scenario results. Never imply a scenario probability unless the platform supplies one. 6. Explain the enterprise lift over CRC: ownership/material assets, broader/finer hazards, multi-factor modelling, future scenarios, aggregation, and benchmarks. -7. Produce a diligence memo with evidence, coverage exceptions, concentrations, drivers, sensitivity, and actions. +7. Produce a diligence memo with evidence, coverage exceptions, concentrations, available drivers, sensitivity, and actions. Never infer drivers from score magnitude or rank. + +## Deliverable bundle + +Retain the unmodified authorized MCP responses, or SDK export, as private JSON +with tool and scenario provenance. Also create flattened analysis-ready CSVs for +the target, scores, and in-scope constituents/assets; create GeoJSON when +authorized locations are returned, plus a self-contained HTML report and +manifest. In the response, show a compact table of total metrics, leading +factors when explicitly returned, concentrations, comparison scenarios, and +coverage exceptions. Label unavailable factor attribution rather than filling +it with climate-score fields. +Include: + +- a map of a disclosed, bounded set of material or highest-risk assets, or + server-provided geographic clusters; and +- factor-attribution charts only when explicit impact/attribution data exists; + otherwise use climate-score distribution, scenario-comparison, and + country/asset-type concentration charts with compatible metrics and explicit + units. + +Keep exhaustive rows in downloadable artifacts rather than crowding the chat. +Do not expose organization-private holdings outside the authorized workspace, +and do not infer coordinates or ownership absent from CDT Express evidence. ## Safety diff --git a/.agents/skills/velo-assess-company-climate-risk/references/playbook.md b/.agents/skills/velo-assess-company-climate-risk/references/playbook.md index c9112ff..b8eb255 100644 --- a/.agents/skills/velo-assess-company-climate-risk/references/playbook.md +++ b/.agents/skills/velo-assess-company-climate-risk/references/playbook.md @@ -12,8 +12,9 @@ For a company: 3. Retrieve owned assets with `get_company_assets` or, only when justified, `get_all_company_assets`. 4. Use `get_asset_climate_scores` on a bounded decision-relevant asset set for - drivers and concentrations. Preserve excluded and failed assets as coverage - exceptions. + risk ranking and geographic or asset-type concentrations after joining asset + metadata. These scores are not factor attribution. Preserve excluded and + failed assets as coverage exceptions. For a market index: @@ -21,14 +22,16 @@ For a market index: 2. Retrieve the total with `get_market_index_climate_scores`. 3. Use `get_market_index_companies_climate_scores` and `get_market_index_assets_climate_scores` with sorting and limits to identify - concentrations; use their `get_all_...` counterparts only for a justified - exhaustive export. + risk ranking and concentrations; do not label them as drivers. Use their + `get_all_...` counterparts only for a justified exhaustive export. Use `get_metrics_definition` when explaining unfamiliar score fields. Complete VELO OAuth and never request `CDT_API_KEY` in chat. If a needed factor, aggregation, or high-risk classification is not exposed by the connected MCP version, state the gap and use the SDK route only when locally configured and -authorized. +authorized. The current climate-score endpoints do not provide factor +attribution; never infer it from score magnitude, ranking, country, or asset +type. ## Local SDK fallback @@ -53,7 +56,7 @@ Use the parallel `client.markets` methods: index lookup/list/search, companies, 1. Entity/index identity and asset snapshot. 2. Scenario and horizon. 3. Total metrics. -4. Factor attribution. +4. Factor attribution from explicit impact output, or a clear unavailable note. 5. Geographic and asset-type concentrations. 6. Tail-risk/high-risk assets. 7. Coverage and identity exceptions. diff --git a/.agents/skills/velo-triage-portfolio-insurability/SKILL.md b/.agents/skills/velo-triage-portfolio-insurability/SKILL.md index bb90f1c..0c99778 100644 --- a/.agents/skills/velo-triage-portfolio-insurability/SKILL.md +++ b/.agents/skills/velo-triage-portfolio-insurability/SKILL.md @@ -49,6 +49,23 @@ never present mocked classifications as live VELO results. 6. Separate the VELO platform classification from the insurer's appetite, pricing, wording, deductibles, reinsurance, and regulatory capital policy. 7. Reconcile with `$crc-model-flood-insurance-loss` only for aligned assets and flood-related evidence; use the comparison to show multi-hazard and enterprise-data lift. +## Deliverable bundle + +Retain the unmodified authorized MCP responses, or SDK export, as private JSON +with scenario and tool provenance. Also create an analysis-ready asset CSV, +GeoJSON when authorized coordinates are available, and a self-contained HTML +report plus manifest. In the response, show a compact table of the highest +priority assets with classification, `cvar_95`, overlap, country, asset type, +scenario, and coverage status. Include: + +- a map with distinct uninsurable, stranded, overlap, and coverage-exception + classes; and +- classification-count and country/asset-type concentration charts. + +Keep the complete portfolio in downloadable artifacts and use a disclosed, +bounded subset only in the visible table/map. Do not expose private asset data +beyond the authorized workspace or invent coordinates for unlocated assets. + ## Safety - Never expose credentials. diff --git a/.agents/skills/velo-underwrite-property-climate/SKILL.md b/.agents/skills/velo-underwrite-property-climate/SKILL.md index 6a11c47..5e3004f 100644 --- a/.agents/skills/velo-underwrite-property-climate/SKILL.md +++ b/.agents/skills/velo-underwrite-property-climate/SKILL.md @@ -50,6 +50,23 @@ never replace enterprise evidence with synthetic data. 6. Compare with the CRC result only after aligning the asset, hazard/factor, horizon/pathway, unit, and metric. Label all non-equivalent results. 7. Write an underwriting evidence memo that distinguishes measured output, platform classification, house policy, and expert judgment. +## Deliverable bundle + +Retain the unmodified authorized MCP responses, or SDK export, as private JSON +with tool names and inputs. Also create a flattened analysis-ready CSV, GeoJSON +when authorized coordinates are returned, and a self-contained HTML report with +a provenance manifest. In the response, show a compact table of scenario, +overall metrics, factor metrics, units, and missing fields. Include: + +- a property map that identifies the resolved asset and spatial precision + without exposing unnecessary private attributes; and +- a factor-contribution or scenario-comparison chart using only semantically + compatible returned metrics. + +Make sanitized table and visual artifacts downloadable when the host permits. +If the service does not return coordinates, show the identity evidence but do +not geocode the address or manufacture a map point without user approval. + ## Safety - Read `RISKTHINKING_API_KEY` from the environment; never request or print it. diff --git a/README.md b/README.md index 998d07a..901727c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ playbook, and only then descend into the open [`crc-framework`](https://pypi.org/project/crc-framework/) implementation. Each open CRC baseline has a higher-calibre VELO/CDT enterprise twin. +Every completed skill run produces both an immediate visual result and a +reusable output bundle: source-of-truth data, an analysis-ready table, spatial +data when available, and a report containing a decision table, chart, and map. + ## Start here: copy a prompt - Link [`RiskThinking/crc-docs`](https://github.com/RiskThinking/crc-docs) into an AI tool that can reference repository files @@ -29,6 +33,15 @@ Use $compare-crc-velo-assessments. The skill will use the paired outputs already in the conversation when they are unambiguous. Advanced users can attach or name the two artifacts explicitly. +### What every run returns + +The chat response shows a compact, unit-labelled results table and at least one +decision-relevant chart. It also shows a map whenever the source contains +coordinates or geometry; missing locations are reported rather than invented. +Downloadable artifacts retain the full rows and provenance for secondary +analysis—typically Parquet or JSON plus CSV, GeoJSON, visual files, and an HTML +report. Enterprise artifacts remain private to the authorized workspace. + If you want the AI to choose for you, begin with: ```text diff --git a/ai-playbooks/examples/dogfood/verify_skills.py b/ai-playbooks/examples/dogfood/verify_skills.py index b1cb861..5be2fce 100644 --- a/ai-playbooks/examples/dogfood/verify_skills.py +++ b/ai-playbooks/examples/dogfood/verify_skills.py @@ -195,9 +195,10 @@ def command(*parts: str | Path) -> subprocess.CompletedProcess[str]: ) -def load_script(skill: str, filename: str) -> Any: - path = SKILLS / skill / "scripts" / filename - spec = importlib.util.spec_from_file_location(f"dogfood_{skill.replace('-', '_')}", path) +def load_module(path: Path) -> Any: + module_name = "dogfood_" + "_".join(path.with_suffix("").parts[-3:]) + module_name = module_name.replace("-", "_") + spec = importlib.util.spec_from_file_location(module_name, path) if spec is None or spec.loader is None: raise RuntimeError(f"cannot load {path}") module = importlib.util.module_from_spec(spec) @@ -205,6 +206,10 @@ def load_script(skill: str, filename: str) -> Any: return module +def load_script(skill: str, filename: str) -> Any: + return load_module(SKILLS / skill / "scripts" / filename) + + def run_mocked(module: Any, argv: list[str], client: FakeClient) -> None: old_argv = sys.argv old_client = module.APIClient @@ -306,11 +311,16 @@ def multi_scenario_flood(destination: Path) -> Path: def dogfood_mortgage(directory: Path, assets: Path) -> dict[str, Any]: script = SKILLS / "crc-screen-mortgage-flood" / "scripts" / "screen_mortgage_flood.py" + skill_module = load_module(script) + pipeline_module = load_module(ROOT / "pipelines" / "jrc_flood_pipeline.py") + skill_resolution = skill_module.parser().get_default("h3_resolution") + pipeline_resolution = pipeline_module.parser().get_default("h3_resolution") + assert skill_resolution == pipeline_resolution == 9 common = [sys.executable, script, "--assets", assets, "--cache", directory / "cache", "--output", directory / "unused.parquet", "--plan-only"] europe = command(*common, "--dataset", "efas", "--bounds", "6.8", "50.8", "7.7", "51.1") canada = command(*common, "--dataset", "glofas", "--bounds", "-79.7", "43.5", "-79.1", "43.9") assert "cems-efas" in europe.stdout and "cems-glofas" in canada.stdout - return {"skill": "crc-screen-mortgage-flood", "targets": ["EFAS/Cologne AOI", "GloFAS/Toronto AOI"], "result": "pass"} + return {"skill": "crc-screen-mortgage-flood", "targets": ["EFAS/Cologne AOI", "GloFAS/Toronto AOI"], "h3_resolution": skill_resolution, "result": "pass"} def dogfood_loss(directory: Path, portfolios: tuple[Path, Path]) -> dict[str, Any]: diff --git a/ai-playbooks/examples/run-open-demo.sh b/ai-playbooks/examples/run-open-demo.sh index 199095a..550535b 100755 --- a/ai-playbooks/examples/run-open-demo.sh +++ b/ai-playbooks/examples/run-open-demo.sh @@ -35,7 +35,7 @@ echo "Sourcing Overture candidates inside the JRC coverage..." uv run python pipelines/overture_assets_pipeline.py \ --bounds "$min_lon" "$min_lat" "$max_lon" "$max_lat" \ --coverage-hazard "$output_dir/jrc_depths_by_cell.parquet" \ - --limit 5 \ + --limit 10 \ --output "$output_dir/overture-assets.csv" echo "Screening the Overture candidates against canonical JRC flood data..." diff --git a/pipelines/jrc_flood_pipeline.py b/pipelines/jrc_flood_pipeline.py index 8e3d8f1..7c89d60 100644 --- a/pipelines/jrc_flood_pipeline.py +++ b/pipelines/jrc_flood_pipeline.py @@ -20,30 +20,34 @@ ) -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( +def parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument( "--bounds", type=float, nargs=4, default=(-79.65, 43.58, -79.15, 43.85), metavar=("MIN_LON", "MIN_LAT", "MAX_LON", "MAX_LAT"), ) - parser.add_argument( + p.add_argument( "--return-periods", type=int, nargs="+", default=[10, 50, 100, 500] ) - parser.add_argument( + p.add_argument( "--dataset", choices=("efas", "glofas"), default="glofas", help="Use EFAS for Europe and GloFAS for global coverage", ) - parser.add_argument("--h3-resolution", type=int, default=9) - parser.add_argument( + p.add_argument("--h3-resolution", type=int, default=9) + p.add_argument( "--cache-mode", choices=("reuse", "offline", "refresh"), default="reuse" ) - parser.add_argument("--output-dir", type=Path, default=Path("pipeline_output")) - return parser.parse_args() + p.add_argument("--output-dir", type=Path, default=Path("pipeline_output")) + return p + + +def parse_args() -> argparse.Namespace: + return parser().parse_args() def main() -> None: From e8ffb3f67b1b094adfb4fd30c5d39a35a61d43c9 Mon Sep 17 00:00:00 2001 From: Runzhou Li Date: Tue, 18 Aug 2026 17:37:08 -0400 Subject: [PATCH 5/5] Add claude skills as symlink, also .mcp.json --- .../velo-underwrite-property-climate/SKILL.md | 14 +++-- .../references/playbook.md | 10 +++- .../scripts/underwrite_property.py | 27 ++++++++- .claude/skills | 1 + .github/workflows/validate-skills.yml | 9 +++ .mcp.json | 8 +++ README.md | 12 +++- .../examples/dogfood/verify_skills.py | 58 +++++++++++++++++-- 8 files changed, 124 insertions(+), 15 deletions(-) create mode 120000 .claude/skills create mode 100644 .mcp.json diff --git a/.agents/skills/velo-underwrite-property-climate/SKILL.md b/.agents/skills/velo-underwrite-property-climate/SKILL.md index 5e3004f..2539799 100644 --- a/.agents/skills/velo-underwrite-property-climate/SKILL.md +++ b/.agents/skills/velo-underwrite-property-climate/SKILL.md @@ -13,11 +13,13 @@ Treat `Use $velo-underwrite-property-climate` as a complete invocation. Reuse a clear property from the conversation; otherwise ask only for a property name, address, or VELO asset ID. Search through the authorized connection, select automatically only when identity is unambiguous, and show a short candidate list -when it is not. Discover supported pathways and horizons internally. Recommend -a decision-relevant scenario or comparison and ask a concise follow-up only when -the choice would materially change the assessment. Do not ask for API IDs or raw -scenario codes that the service can discover. Valid advanced inputs override -defaults. +when it is not. For every bounded candidate list, disclose the returned count +and whether more matches exist. If the list is truncated, ask for a refined +query instead of presenting it as exhaustive. Discover supported pathways and +horizons internally. Recommend a decision-relevant scenario or comparison and +ask a concise follow-up only when the choice would materially change the +assessment. Do not ask for API IDs or raw scenario codes that the service can +discover. Valid advanced inputs override defaults. ## CDT Express MCP execution @@ -42,7 +44,7 @@ never replace enterprise evidence with synthetic data. ## Workflow -1. Require an existing VELO `asset_id` or search query. If search returns ambiguity, show candidates and stop for selection. +1. Require an existing VELO `asset_id` or search query. If search returns ambiguity, show candidates and stop for selection. State whether the result is truncated; refine or paginate the search before selection when it is. 2. Discover valid pathways and horizons from the connected tool or SDK. Never guess an allowed value. 3. Resolve the asset and its owner. The public `velo-sdk` 0.0.20 reaches asset climate/impact scores through the owning company's asset lists; see [references/playbook.md](references/playbook.md). 4. Run `scripts/underwrite_property.py` for a read-only SDK assessment, or use semantically equivalent authorized MCP tools. diff --git a/.agents/skills/velo-underwrite-property-climate/references/playbook.md b/.agents/skills/velo-underwrite-property-climate/references/playbook.md index 4ac1dae..77d3814 100644 --- a/.agents/skills/velo-underwrite-property-climate/references/playbook.md +++ b/.agents/skills/velo-underwrite-property-climate/references/playbook.md @@ -7,7 +7,10 @@ chat-native run: 1. Inspect the available tools and their schemas; supported scenario values are defined there and can change by server version. -2. Call `search_assets` for a name/address, or `get_asset` for a known ID. +2. Call `search_assets` for a name/address, or `get_asset` for a known ID. Honor + pagination, limits, and truncation metadata returned by the server. If only a + bounded subset is shown, disclose that it is partial and refine or paginate + the search before asking the user to select an asset. 3. Call `get_asset_climate_scores` for the chosen pathway and horizon. 4. When location-level factor evidence is needed and supported, use `get_climate_metrics_exposure`, `get_climate_metrics_impact`, @@ -28,7 +31,10 @@ In `crc-docs`, install the optional client with `uv sync --extra velo`, then run ## Read-only route in velo-sdk 0.0.20 -1. Search or get the asset. +1. Search or get the asset. The bundled script reads at most 11 matches, returns + at most 10 candidates, and reports `truncated: true` plus a match-count lower + bound when more exist. Refine the query rather than treating that list as + complete. 2. Resolve its owner with `client.assets.get_asset_owner(asset_id)`. 3. Read `client.companies.list_company_asset_climate_scores(owner.id, pathway, horizon)` and select the matching `asset_id`. 4. Read `client.companies.list_company_asset_impact_scores(...)` and select the same asset. diff --git a/.agents/skills/velo-underwrite-property-climate/scripts/underwrite_property.py b/.agents/skills/velo-underwrite-property-climate/scripts/underwrite_property.py index 2c93a92..f5efda2 100644 --- a/.agents/skills/velo-underwrite-property-climate/scripts/underwrite_property.py +++ b/.agents/skills/velo-underwrite-property-climate/scripts/underwrite_property.py @@ -41,7 +41,31 @@ def main() -> int: else: candidates = list(islice(client.assets.search_assets(args.query), 11)) if len(candidates) != 1: - print(json.dumps({"status": "selection_required", "candidates": [dump(item) for item in candidates[:10]]}, indent=2)) + visible_candidates = candidates[:10] + truncated = len(candidates) > len(visible_candidates) + print( + json.dumps( + { + "status": "selection_required", + "query": args.query, + "returned_count": len(visible_candidates), + "match_count": None if truncated else len(candidates), + "match_count_lower_bound": len(candidates), + "truncated": truncated, + "message": ( + "Showing the first 10 matches; refine the query before selecting an asset." + if truncated + else ( + "Select one of the returned matches." + if candidates + else "No matching assets found; revise the query." + ) + ), + "candidates": [dump(item) for item in visible_candidates], + }, + indent=2, + ) + ) return 2 asset = candidates[0] @@ -66,4 +90,3 @@ def main() -> int: if __name__ == "__main__": raise SystemExit(main()) - diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000..2b7a412 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.github/workflows/validate-skills.yml b/.github/workflows/validate-skills.yml index fa63930..62b14ff 100644 --- a/.github/workflows/validate-skills.yml +++ b/.github/workflows/validate-skills.yml @@ -4,12 +4,16 @@ on: pull_request: paths: - ".agents/skills/**" + - ".claude/skills" + - ".mcp.json" - ".github/workflows/validate-skills.yml" push: branches: - main paths: - ".agents/skills/**" + - ".claude/skills" + - ".mcp.json" - ".github/workflows/validate-skills.yml" workflow_dispatch: @@ -30,6 +34,11 @@ jobs: shell: bash run: | set -euo pipefail + if [[ ! -L .claude/skills ]] || [[ "$(readlink .claude/skills)" != "../.agents/skills" ]]; then + echo ".claude/skills must point to ../.agents/skills" >&2 + exit 1 + fi + python -m json.tool .mcp.json >/dev/null shopt -s nullglob skills=(.agents/skills/*) if (( ${#skills[@]} == 0 )); then diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..c8a8ff9 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "cdt-express": { + "type": "http", + "url": "https://mcp.riskthinking.ai/mcp" + } + } +} diff --git a/README.md b/README.md index 901727c..bf2be51 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,11 @@ data when available, and a report containing a decision table, chart, and map. - Start with only the skill name; the AI will ask for the minimum information it needs - Optionally include the target or advanced assumptions you already know -Note: to run an enterprise twin, connect the [`CDT Express MCP server`](https://github.com/RiskThinking/cdt-express-mcp) in your AI tool using `https://mcp.riskthinking.ai/mcp`, complete the VELO OAuth flow, and enable its tools for the chat. Do not paste an API key into the conversation. Each VELO/CDT skill declares this remote server as a dependency, discovers its supported tool schemas, and guides the relevant read-only calls. +The table uses Codex's `$skill-name` selector. In ChatGPT, select the skill with +`@`; in Claude Code, use `/skill-name`. Plain language such as +`Use the crc-screen-mortgage-flood skill` is the portable fallback. + +Note: to run an enterprise twin, connect the [`CDT Express MCP server`](https://github.com/RiskThinking/cdt-express-mcp) in your AI tool using `https://mcp.riskthinking.ai/mcp`, complete the VELO OAuth flow, and enable its tools for the chat. Do not paste an API key into the conversation. OpenAI reads the dependency from each enterprise skill's `agents/openai.yaml`; Claude Code reads the project-level [`.mcp.json`](.mcp.json). Each skill discovers the connected tool schemas and guides the relevant read-only calls. | Decision | Open CRC baseline | VELO/CDT enterprise twin | |---|---|---| @@ -48,6 +52,12 @@ If you want the AI to choose for you, begin with: Which CRC/VELO skill should I use for my climate-risk question? ``` +### Platform discovery + +- **ChatGPT/Codex:** the canonical skills live under [`.agents/skills`](.agents/skills); `agents/openai.yaml` adds OpenAI presentation metadata and the enterprise MCP dependency. +- **Claude Code:** [`.claude/skills`](.claude/skills) points to the same canonical folders, so the skills are auto-discovered without duplicated playbooks. Approve the project MCP server and use `/mcp` once to complete VELO OAuth. +- **claude.ai:** upload the desired canonical skill folder as a custom Skill and add CDT Express as a custom connector for enterprise runs. Project `.mcp.json` applies to Claude Code, not the claude.ai upload. + ## Peel down into the playbooks A bare skill invocation starts a guided intake. The selected `SKILL.md` owns the diff --git a/ai-playbooks/examples/dogfood/verify_skills.py b/ai-playbooks/examples/dogfood/verify_skills.py index 5be2fce..71e1abb 100644 --- a/ai-playbooks/examples/dogfood/verify_skills.py +++ b/ai-playbooks/examples/dogfood/verify_skills.py @@ -6,10 +6,12 @@ import argparse import csv import importlib.util +import io import json import subprocess import sys import tempfile +from contextlib import redirect_stdout from pathlib import Path from typing import Any @@ -210,18 +212,26 @@ def load_script(skill: str, filename: str) -> Any: return load_module(SKILLS / skill / "scripts" / filename) -def run_mocked(module: Any, argv: list[str], client: FakeClient) -> None: +def run_mocked( + module: Any, + argv: list[str], + client: FakeClient, + expected_return_codes: tuple[int | None, ...] = (None, 0), +) -> str: old_argv = sys.argv old_client = module.APIClient + stdout = io.StringIO() try: module.APIClient = lambda: client sys.argv = [str(module.__file__), *argv] - result = module.main() - if result not in (None, 0): + with redirect_stdout(stdout): + result = module.main() + if result not in expected_return_codes: raise AssertionError(f"unexpected return code {result}") finally: module.APIClient = old_client sys.argv = old_argv + return stdout.getvalue() def write_asset_targets(directory: Path) -> tuple[Path, Path]: @@ -359,7 +369,47 @@ def dogfood_property(directory: Path, client: FakeClient) -> tuple[dict[str, Any alpha = json.loads(output_a.read_text()) beta = json.loads(output_b.read_text()) assert alpha["asset"]["id"] == "asset-a" and beta["asset"]["id"] == "asset-b" - evidence = {"skill": "velo-underwrite-property-climate", "targets": ["asset ID/Cologne warehouse/2050", "search/Toronto plant/2070"], "result": "pass (API double)"} + for number in range(12): + asset_id = f"overflow-{number}" + client.assets.items[asset_id] = Record( + id=asset_id, + name=f"Overflow Property {number}", + address=f"{number} Example Road", + latitude=43.65, + longitude=-79.38, + asset_type="office", + ) + selection_output = run_mocked( + module, + [ + "--query", + "Overflow Property", + "--pathway", + "ssp245", + "--horizon", + "2050", + "--output", + str(directory / "unused.json"), + ], + client, + expected_return_codes=(2,), + ) + selection = json.loads(selection_output) + assert selection["status"] == "selection_required" + assert selection["returned_count"] == len(selection["candidates"]) == 10 + assert selection["truncated"] is True + assert selection["match_count"] is None + assert selection["match_count_lower_bound"] == 11 + assert "refine" in selection["message"].lower() + evidence = { + "skill": "velo-underwrite-property-climate", + "targets": [ + "asset ID/Cologne warehouse/2050", + "search/Toronto plant/2070", + "ambiguous search with more than 10 matches", + ], + "result": "pass (API double)", + } return evidence, output_a, output_b