Chapter 13·Part IVPricing and underwriting

Pricing and Underwriting

Commercial property underwriting for Meridian Re: COPE extraction from a broker submission PDF, GLM-vs-comparables premium reconciliation, and the three-agent underwriting workflow.

In the book this is Part IV, Agentic AI in Actuarial Practice.

What you’ll build

  • Automated rating with the rating model retained as the actuarial control point
  • Underwriting triage against underwriting authority
  • Competitive intelligence from public rate filings and market reports
  • Dynamic pricing cycles compressed from quarterly to weekly
  • Continuous model validation, back-testing, and sensitivity analysis
  • Rate filing compliance checks with citations to source provisions

01_cope_extraction_tool.py

run in ColabSource on GitHub

Extracts COPE fields from the broker submission PDF via pypdf.

# Submission extraction tool — focuses on the actuarial domain logic
# of COPE schema validation and the structured status return.
# Book reference: Chapter 13, "The Three-Agent Workflow"
# Repo note: the _read_submission_pdf / _parse_cope_schema /
# _has_all_cope_families helpers live in support.py.
import os

from support import _has_all_cope_families, _parse_cope_schema, _read_submission_pdf

DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")


def extract_cope_attributes(submission_pdf_path: str) -> dict:
    """Extract COPE attributes from a commercial property submission PDF.

    COPE — Construction, Occupancy, Protection, Exposure — is the locked
    underwriting schema. The extracted attributes feed the rating model
    and the comparable-account search.

    Args:
        submission_pdf_path: filesystem path to the broker's submission PDF.

    Returns:
        Dict with status, cope_data, provenance, and note fields.
    """
    try:
        # Helpers from the firm's submission-handling library
        submission_text = _read_submission_pdf(submission_pdf_path)
        cope_data = _parse_cope_schema(submission_text)

        # Validation gate: all four COPE families must populate
        if not _has_all_cope_families(cope_data):
            return {
                "status": "out_of_range",
                "cope_data": None,
                "provenance": {"source_path": submission_pdf_path},
                "note": "missing one or more COPE families; route to underwriter",
            }

        return {
            "status": "ok",
            "cope_data": cope_data,
            "provenance": {
                "source_path": submission_pdf_path,
                "extraction_method": "gemini_structured_v1",
            },
            "note": None,
        }
    except Exception as exc:
        # Operational exceptions never propagate to the agent runtime;
        # they are caught and returned as an explicit error status.
        return {
            "status": "error",
            "cope_data": None,
            "provenance": {"source_path": submission_pdf_path},
            "note": f"extraction failed: {exc}",
        }


if __name__ == "__main__":
    submission = os.path.join(DATA_DIR, "submissions", "MR-CHI-2025-Q3-018.pdf")
    result = extract_cope_attributes(submission)
    print(result["status"])
    print(result["cope_data"])

This script needs packages beyond the browser runtime. Open the chapter in Colab to run it with your own free Gemini key.

02_pricing_reconciliation_tool.py

runs in your browserSource on GitHub

Reconciles a cedent's proposed rate against internal pricing.

02_pricing_reconciliation_tool.py
# Generated from ch13_underwriting_agent/02_pricing_reconciliation_tool.py for in-browser execution.
# Agno agent wiring is removed so the tool runs as a plain function;
# the full version is on GitHub. Do not edit: regenerate with
# scripts/build_demos.py.
# Pricing reconciliation tool — GLM technical premium vs comparable median.
# Book reference: Chapter 13, "The Three-Agent Workflow"


def compare_to_pricing_model(
    emblem_radar_premium_usd: float,
    comparable_median_premium_usd: float,
    target_loss_ratio: float,
) -> dict:
    """Reconcile the GLM technical premium against the comparable-account median.

    A reconciliation gap above the firm's 10% threshold flags the submission
    for senior underwriter review before the recommendation enters Sophie
    Laurent's queue. Below threshold, the agent produces a draft with both
    anchors visible.

    Args:
        emblem_radar_premium_usd: technical premium from the GLM (USD).
        comparable_median_premium_usd: median of three comparable accounts (USD).
        target_loss_ratio: portfolio target for the renewal cycle.

    Returns:
        Dict with status, reconciliation_gap_pct, recommended_premium_usd,
        provenance, and note fields.
    """
    # Compute the percentage gap between the two technical-premium anchors
    gap_pct = abs(emblem_radar_premium_usd - comparable_median_premium_usd) \
              / comparable_median_premium_usd * 100

    # Threshold encodes the firm's reconciliation tolerance
    if gap_pct > 10.0:
        return {
            "status": "out_of_range",
            "reconciliation_gap_pct": round(gap_pct, 2),
            "recommended_premium_usd": None,
            "provenance": {
                "glm_method": "emblem_radar_v4.2",
                "comparable_method": "marketview_p_2025q3",
            },
            "note": "gap above 10% threshold; route to senior underwriter",
        }

    # Within tolerance — return midpoint as the draft anchor
    midpoint_premium = (emblem_radar_premium_usd + comparable_median_premium_usd) / 2

    return {
        "status": "ok",
        "reconciliation_gap_pct": round(gap_pct, 2),
        "recommended_premium_usd": round(midpoint_premium, 2),
        "provenance": {
            "glm_method": "emblem_radar_v4.2",
            "comparable_method": "marketview_p_2025q3",
            "target_loss_ratio": target_loss_ratio,
        },
        "note": None,
    }


if __name__ == "__main__":
    print(compare_to_pricing_model(86_000.0, 82_500.0, 0.62))   # within tolerance
    print(compare_to_pricing_model(86_000.0, 71_000.0, 0.62))   # out_of_range path

03_underwriting_workflow.py

live agent on GeminiSource on GitHub

Extraction, pricing, and review agents process the submission end to end.

about 90s · live model callOpen in Colab
# Three-agent underwriting workflow — fixed path.
# Book reference: Chapter 13, "The Three-Agent Workflow"
# Repo note: tools referenced "from the firm's library" live in
# support.py and the sibling listing modules.
import importlib

from agno.agent import Agent
import os
import sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))  # for common/
from common.config import get_model
from agno.workflow import Workflow, Step

from support import (
    fetch_emblem_radar_premium,
    parse_loss_summary,
    query_internal_loss_db,
    query_marketview_aggregator,
)

# Numeric module names can't be imported with a plain import statement.
extract_cope_attributes = importlib.import_module(
    "01_cope_extraction_tool").extract_cope_attributes
compare_to_pricing_model = importlib.import_module(
    "02_pricing_reconciliation_tool").compare_to_pricing_model

# Three specialised agents — each with a narrow, named scope
submission_agent = Agent(
    model=get_model(),
    tools=[extract_cope_attributes, parse_loss_summary],
    tool_call_limit=10,  # bound the agent loop; case-study default
    instructions="Extract COPE attributes and prior loss history.",
    markdown=True,
)

market_data_agent = Agent(
    model=get_model(),
    tools=[query_internal_loss_db, query_marketview_aggregator],
    tool_call_limit=10,
    instructions="Retrieve comparable internal claims and market benchmarks.",
    markdown=True,
)

pricing_comparison_agent = Agent(
    model=get_model(),
    tools=[fetch_emblem_radar_premium, compare_to_pricing_model],
    tool_call_limit=10,
    instructions="Compare GLM technical premium against comparable-account median.",
    markdown=True,
)

# Fixed-path workflow — the sequence is decided in advance, not at runtime
underwriting_workflow = Workflow(
    name="commercial_property_underwriting",
    steps=[
        Step(name="extraction", agent=submission_agent),
        Step(name="market_data", agent=market_data_agent),
        Step(name="pricing_comparison", agent=pricing_comparison_agent),
    ],
)

if __name__ == "__main__":
    # Run the workflow on a single submission. The path is resolved from
    # this file, not the working directory, so the script runs the same
    # from the repo root as from the chapter folder.
    submission_pdf = os.path.join(
        os.path.dirname(os.path.abspath(__file__)),
        "..", "data", "submissions", "MR-CHI-2025-Q3-018.pdf",
    )
    underwriting_workflow.print_response(
        f"Process submission {submission_pdf} "
        "(reference MR-CHI-2025-Q3-018) and produce a draft recommendation.",
        markdown=True,
        stream=True,
    )

Runs the unmodified chapter script on the server and streams the agent's tool calls and reasoning here.

Shared helper modules for this chapter live in support.py.