Chapter 10·Part IIIAgentic architecture

Tool Use and Function Calling

Designing actuarial tools: a mortality lookup with a validation gate, a present-value tool with structured error handling, and the term life premium agent that chains them together.

In the book this is Part III, Agentic AI: From Concept to Architecture.

What you’ll build

  • A tool as a named function with a specified input schema and output
  • Why tool descriptions drive reliability more than prompt engineering does
  • Tool selection failure, and keeping the tool set to five to ten tools
  • Structured errors that let an agent retry, fall back, or escalate
  • Sandboxing, least capability, and the blast radius of a tool call
  • Code execution tools as the highest-power, highest-risk case

01_mortality_tool.py

runs in your browserSource on GitHub

Looks up q(x) from the IALM 2012-14 ULP table, rejecting out-of-range inputs.

01_mortality_tool.py
# Generated from ch10_tool_use/01_mortality_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.
# ── Tool: mortality lookup (IALM 2012-14 ULP) ───────────────────
# Book reference: Chapter 10, §10.2 "Designing Actuarial Tools"
# Repo note: _ialm_lookup comes from support.py (synthetic table); the
# book describes it as "existing firm library".
from typing import Literal                          # Type hint constraints

from support import _ialm_lookup                    # repo-supplied helper


def lookup_mortality_rate(
    age: int,
    gender: Literal["M", "F"],
    smoker_status: Literal["smoker", "non_smoker"],
) -> dict:
    """Return one-year mortality rate q_x from the IALM 2012-14 ULP table.

    Use for term life and whole life net premium reserve calculations on
    India business. Do not use for annuitant mortality — call
    lookup_annuitant_mortality instead.

    Args:
        age: Attained age in completed years. Valid range 18-99.
        gender: "M" or "F".
        smoker_status: "smoker" or "non_smoker".

    Returns:
        dict with keys:
            mortality_rate: float — q_x value (e.g., 0.00121)
            table_name: str — source table identifier
            table_version: str — version stamp
            status: str — "ok" or "out_of_range"
    """
    if not 18 <= age <= 99:                         # Input validation gate
        return {
            "mortality_rate": None,
            "table_name": "IALM_2012_14_ULP",
            "table_version": "v1.0",
            "status": "out_of_range",
        }
    rate = _ialm_lookup(age, gender, smoker_status) # Existing firm library
    return {
        "mortality_rate": rate,
        "table_name": "IALM_2012_14_ULP",
        "table_version": "v1.0",
        "status": "ok",
    }


if __name__ == "__main__":
    # Direct tool exercise — no agent needed to test a tool in isolation.
    print(lookup_mortality_rate(35, "M", "non_smoker"))
    print(lookup_mortality_rate(105, "M", "non_smoker"))  # out_of_range path

02_present_value_tool.py

runs in your browserSource on GitHub

Discounts a stream of future cashflows with structured error returns.

02_present_value_tool.py
# Generated from ch10_tool_use/02_present_value_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.
# ── Tool: present value with structured error handling ───────────────
# Book reference: Chapter 10, §10.4 "Error Handling and Recovery"
from datetime import date


def calculate_present_value(
    cashflows_inr: list[float],
    discount_rate_annual: float,
    valuation_date: str,
) -> dict:
    """Compute the present value of a cashflow stream at a constant rate.

    Args:
        cashflows_inr: per-year cashflow amounts, year 1 onward.
        discount_rate_annual: e.g., 0.06 for 6% per annum.
        valuation_date: ISO date string YYYY-MM-DD.

    Returns:
        dict with present_value_inr, method, status, note.
    """
    try:
        if not 0.0 <= discount_rate_annual <= 0.20:    # Sanity bound
            return {
                "present_value_inr": None,
                "method": "level_pv_v1.2",
                "status": "out_of_range",
                "note": f"Discount rate {discount_rate_annual} outside [0, 0.20]",
            }
        date.fromisoformat(valuation_date)             # Date format check
        pv = sum(
            cf / (1 + discount_rate_annual) ** (year + 1)
            for year, cf in enumerate(cashflows_inr)
        )
        return {
            "present_value_inr": round(pv, 2),
            "method": "level_pv_v1.2",
            "status": "ok",
            "note": None,
        }
    except (ValueError, TypeError) as exc:             # Structured error return
        return {
            "present_value_inr": None,
            "method": "level_pv_v1.2",
            "status": "error",
            "note": f"Input validation failed: {exc}",
        }


if __name__ == "__main__":
    print(calculate_present_value([100000.0] * 5, 0.06, "2025-03-31"))
    print(calculate_present_value([100000.0] * 5, 0.45, "2025-03-31"))   # out_of_range
    print(calculate_present_value([100000.0] * 5, 0.06, "31/03/2025"))   # error path

03_term_life_premium_agent.py

live agent on GeminiSource on GitHub

The agent chains both tools to price a 10-year term assurance.

about 30s · live model callOpen in Colab
# ── Chapter 10 Illustrative Case Study: term life net premium agent ──
# Book reference: Chapter 10, "Illustrative Case Study" (Tools 1-3 + agent)
# Repo note: _ialm_lookup and _experience_lookup come from support.py.
from typing import Literal

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 support import _experience_lookup, _ialm_lookup


# ── Tool 1: mortality lookup (IALM 2012-14 ULP, India) ─────────────
def lookup_mortality_rate(
    age: int,
    gender: Literal["M", "F"],
    smoker_status: Literal["smoker", "non_smoker"],
) -> dict:
    """Return one-year mortality rate q_x from IALM 2012-14 ULP.

    Use for term life net premium reserve calculations on India business.
    Do not use for annuitant mortality.

    Args:
        age: Attained age in completed years. Range 18-99.
        gender: "M" or "F".
        smoker_status: "smoker" or "non_smoker".

    Returns:
        dict with mortality_rate, table_name, table_version, status.
    """
    if not 18 <= age <= 99:
        return {"mortality_rate": None, "table_name": "IALM_2012_14_ULP",
                "table_version": "v1.0", "status": "out_of_range"}
    rate = _ialm_lookup(age, gender, smoker_status)   # firm's library
    return {"mortality_rate": rate, "table_name": "IALM_2012_14_ULP",
            "table_version": "v1.0", "status": "ok"}


# ── Tool 2: experience adjustment query ───────────────────────
def query_experience_study(
    line_of_business: Literal["term_life", "whole_life"],
    study_year: int,
) -> dict:
    """Return the firm's experience adjustment factor for a line and year.

    Adjustment is multiplicative on base table rates: 1.00 = no adjustment,
    0.95 = actual experience 95% of table.

    Args:
        line_of_business: "term_life" or "whole_life".
        study_year: Year of the experience study (e.g., 2023).

    Returns:
        dict with adjustment_factor, study_id, status.
    """
    record = _experience_lookup(line_of_business, study_year)
    if record is None:
        return {"adjustment_factor": None, "study_id": None,
                "status": "not_found"}
    return {"adjustment_factor": record.factor,
            "study_id": record.study_id, "status": "ok"}


# ── Tool 3: present value of cashflows ────────────────────────
def calculate_present_value(
    cashflows_inr: list[float],
    discount_rate_annual: float,
) -> dict:
    """Discount a stream of annual cashflows at a constant rate.

    Args:
        cashflows_inr: per-year cashflow amounts, year 1 onward.
        discount_rate_annual: annual rate, e.g., 0.06 for 6%.

    Returns:
        dict with present_value_inr, method, status.
    """
    if not 0.0 <= discount_rate_annual <= 0.20:
        return {"present_value_inr": None, "method": "level_pv_v1.2",
                "status": "out_of_range"}
    pv = sum(cf / (1 + discount_rate_annual) ** (yr + 1)
             for yr, cf in enumerate(cashflows_inr))
    return {"present_value_inr": round(pv, 2),
            "method": "level_pv_v1.2", "status": "ok"}


# ── Agent: term life net premium calculator ──────────────────
term_life_agent = Agent(
    model=get_model(),
    tools=[
        lookup_mortality_rate,
        query_experience_study,
        calculate_present_value,
    ],
    description=(
        "Calculate the net annual level premium for a term life policy "
        "using IALM 2012-14 ULP mortality, the firm's experience adjustment, "
        "and a stated discount rate. Return the premium with full provenance."
    ),
    instructions=[
        "Look up mortality rates for each policy year from issue age to "
        "issue age + term - 1.",
        "Apply the term_life experience adjustment from the most recent "
        "available study year.",
        "Compute the expected present value of death benefits and the "
        "premium annuity using the supplied discount rate.",
        "Return the net annual level premium with the tool calls used.",
    ],
    tool_call_limit=10,
    markdown=True,
)

if __name__ == "__main__":
    term_life_agent.print_response(
        "Compute the net annual level premium for a 10-year term policy on a "
        "35-year-old male non-smoker, INR 50,00,000 sum assured, 6% discount rate.",
        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.