Chapter 14·Part IVReserving and claims

Reserving and Claims

Reserving with reflexion: a Cape Cod tool that derives its expected loss ratio from the data, movement commentary that cites its sources, and the full reflexion workflow that reconciles against the prior cycle.

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

What you’ll build

  • Multiple reserving methods applied to one triangle, with agreement and disagreement surfaced
  • Claims triage and routing from free-text notes
  • Fraud detection that produces investigative leads, not adjudications
  • Continuous loss development monitoring against prior expectations
  • Narrative generation for reserving memoranda
  • The shift from an episodic quarterly close to continuous monitoring

tools_reserving.py

runs in your browserSource on GitHub

Cape Cod reserve estimate on the Meridian motor triangle.

tools_reserving.py
# Generated from ch14_reserving_reflexion/tools_reserving.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.
# meridian_re/tools/reserving.py
# Book reference: Chapter 14, "Code walkthrough"
# Repo note: load_triangle, compute_cape_cod_elr, apply_cape_cod_blend
# live in support.py (the book attributes them to the standard helper
# library used across CL/BF tools).

from support import apply_cape_cod_blend, compute_cape_cod_elr, load_triangle


def cape_cod(
    triangle_table: str = "meridian_claims.triangle_motor_india",
    accident_year_start: int = 2018,
    accident_year_end: int = 2023,
) -> dict:
    """Run the Cape Cod method on the motor India triangle.

    Returns the Cape Cod ultimate, the data-derived expected loss
    ratio, and the segment weights used in the credibility blend.
    The data-derived ELR distinguishes Cape Cod from BF, which
    relies on an externally assumed a-priori ELR.

    Args:
        triangle_table: Fully qualified triangle table name.
        accident_year_start: First AY in scope (inclusive).
        accident_year_end: Last AY in scope (inclusive).

    Returns:
        dict with status, ultimate_loss, derived_elr, segment_weights,
        provenance metadata, and note.
    """
    try:
        # Standard helper used across CL/BF tools.
        triangle_df = load_triangle(
            triangle_table, accident_year_start, accident_year_end
        )
        # Cape Cod blend: derive ELR from data, weight by exposure.
        derived_elr, weights = compute_cape_cod_elr(triangle_df)
        ultimate_loss = apply_cape_cod_blend(triangle_df, derived_elr)
        return {
            "status": "ok",
            "ultimate_loss": ultimate_loss,
            "derived_elr": derived_elr,
            "segment_weights": weights,
            "method": "cape_cod_v1.0",
            "table": triangle_table,
            "accident_years": (accident_year_start, accident_year_end),
            "note": None,
        }
    except Exception as caught_exception:
        # Operational exceptions converted to a structured result;
        # never propagate to the agent runtime.
        return {
            "status": "error",
            "ultimate_loss": None,
            "derived_elr": None,
            "segment_weights": None,
            "method": "cape_cod_v1.0",
            "table": triangle_table,
            "accident_years": (accident_year_start, accident_year_end),
            "note": str(caught_exception),
        }


if __name__ == "__main__":
    # Exercise the tool function directly (bypassing the agent).
    print(cape_cod())

tools_commentary.py

runs in your browserSource on GitHub

Drafts movement commentary from a reflexion check's output.

tools_commentary.py
# Generated from ch14_reserving_reflexion/tools_commentary.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.
# meridian_re/tools/commentary.py
# Book reference: Chapter 14, "Code walkthrough"
# Repo note: draft_deviation_paragraph and draft_stable_paragraph live
# in support.py. For an agent exercising this tool, see
# 02_commentary_agent.py.

from support import draft_deviation_paragraph, draft_stable_paragraph


def draft_movement_commentary(
    reasoning_trace: dict,
    reflexion_output: dict,
    prior_cycle_decisions: dict,
) -> dict:
    """Draft a structured reserve-movement commentary paragraph.

    Every numerical claim in the output traces to one of the three
    inputs through the citations field. The Fellow reviews the
    paragraph against the trace, not against surface fluency.

    Args:
        reasoning_trace: Inner workflow trace (inputs/outputs/rationale).
        reflexion_output: Reconciliation result with deviation detail.
        prior_cycle_decisions: Stored record from prior cycle.

    Returns:
        dict with status, paragraph, citations, and note.
    """
    if reflexion_output.get("status") != "ok":
        # Out-of-range deviations get a deviation paragraph template
        # that surfaces the deviation detail explicitly.
        return draft_deviation_paragraph(
            reasoning_trace, reflexion_output, prior_cycle_decisions
        )
    return draft_stable_paragraph(
        reasoning_trace, reflexion_output, prior_cycle_decisions
    )


if __name__ == "__main__":
    sample_reflexion = {"status": "ok", "actual_ldf_12_24": 1.56,
                        "expected_ldf_12_24": 1.55, "deviation_pct": 0.65,
                        "tolerance_pct": 1.5}
    print(draft_movement_commentary(
        reasoning_trace={}, reflexion_output=sample_reflexion,
        prior_cycle_decisions={}))

01_reserving_reflexion_workflow.py

live agent on GeminiSource on GitHub

The workflow critiques its own diagnostics before concluding.

about 90s · live model callOpen in Colab
# meridian_re/workflows/reserving_reflexion.py
# Book reference: Chapter 14, "Code walkthrough"
#
# ⚠ API COMPATIBILITY NOTE (see ERRATA in the root README):
# The printed listing uses `from agno.memory import Memory` and
# `Agent(memory=Memory(db=SqliteDb(...)))`. Later Agno 2.x releases
# replaced that with `Agent(db=SqliteDb(...), enable_user_memories=True)`,
# used below — the persistence behaviour is unchanged.
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
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 (
    build_reserving_review_workflow,
    reconcile_against_developed_losses,
    retrieve_prior_cycle,
)

# Inner workflow reused from Chapter 11, with Cape Cod added.
inner_workflow = build_reserving_review_workflow(
    include_cape_cod=True
)

# Reconciliation agent: reads next-cycle developed losses, retrieves
# prior cycle's LDF expectations from stored memory, runs the
# reconcile_against_developed_losses tool and returns a structured
# result.
reconciliation_agent = Agent(
    name="reconciliation",
    model=get_model(),
    tools=[reconcile_against_developed_losses, retrieve_prior_cycle],
    db=SqliteDb(db_file="meridian_reserving_memory.db"),  # was: memory=Memory(db=...)
    enable_user_memories=True,
    tool_call_limit=10,
    markdown=True,
    instructions=(
        "First call retrieve_prior_cycle to get the prior cycle's LDF "
        "expectations. Then take the observed 12-24 development factor "
        "from the reserving step's output and call "
        "reconcile_against_developed_losses to compare actual against "
        "expected. Surface deviations exceeding the firm's 1.5 percent "
        "tolerance. Do not revise reserves; route deviations to the "
        "reserving actuary's review queue."
    ),
)

reserving_reflexion_workflow = Workflow(
    name="ReservingReflexion",
    steps=[
        Step(name="act", workflow=inner_workflow),
        Step(name="evaluate", agent=reconciliation_agent),
    ],
)

if __name__ == "__main__":
    # The input names no tools: it is passed verbatim to the inner
    # workflow's agents, which do not have the reconciliation tools —
    # naming those here makes the inner agents attempt calls to
    # functions they don't own ("Function ... not found"). The evaluate
    # step's tool sequence lives in the reconciliation agent's
    # instructions instead.
    reserving_reflexion_workflow.print_response(
        "Run the FY2024 Q3 motor India reserving cycle and report the "
        "reserve estimates, development factors, and reconciliation.",
        markdown=True,
        stream=True,
    )

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

02_commentary_agent.py

live agent on GeminiSource on GitHub

An agent exercises the commentary tool end to end.

about 30s · live model callOpen in Colab
# Repo demo (not a printed listing): a simple commentary agent
# exercising the Chapter 14 draft_movement_commentary tool. The tool
# itself, and its direct test, live in tools_commentary.py — run that
# first to see the raw cited-paragraph dict the agent consumes here.
import os

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

from tools_commentary import draft_movement_commentary

# The reserving actuary supplies the reconciliation figures; the agent
# drafts the citation-backed paragraph through the tool. The prompt and
# instructions deliberately avoid echoing the tool's name in prose:
# "reserve-movement commentary" phrasing adjacent to the identifier once
# primed Gemini into calling a non-existent
# draft_movement_movement_commentary. tool_choice="validated" (a
# Gemini-only function-calling mode; other providers would reject it)
# additionally constrains the model to the declared tool names.
commentary_agent = Agent(
    model=get_model(),
    tools=[draft_movement_commentary],
    tool_call_limit=3,
    markdown=True,
    tool_choice="validated" if os.getenv("MODEL_PROVIDER", "google").lower() == "google" else None,
    instructions=(
        "Call draft_movement_commentary once, passing the reconciliation "
        "figures from the request as the reflexion_output dict (use empty "
        "dicts for reasoning_trace and prior_cycle_decisions if none are "
        "supplied). Return the tool's paragraph and citations verbatim. "
        "Only call the tools you have been given."
    ),
)

if __name__ == "__main__":
    sample_reflexion = {"status": "ok", "actual_ldf_12_24": 1.56,
                        "expected_ldf_12_24": 1.55, "deviation_pct": 0.65,
                        "tolerance_pct": 1.5}
    commentary_agent.print_response(
        f"Reconciliation figures for this cycle: {sample_reflexion}. "
        "Produce the cited paragraph with the tool.",
        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.