.. _concept_ai_agents:
AI Agents
=========
pyRadPlan ships an optional ``ai_agents`` module that uses
`pydantic-ai `_ to integrate large-language-model (LLM) reasoning
into the treatment planning workflow. The agents follow the same pydantic data-model
conventions as the rest of pyRadPlan and return fully-validated plan and structure-set
objects, so their output can be fed directly into the rest of the pipeline.
.. note::
The ``ai_agents`` module is an optional feature. Before using it you need to install
the optional dependencies and set an API key for your chosen LLM provider:
.. code-block:: bash
pip install pydantic-ai pydantic-settings python-dotenv
Then export your provider API key, for example:
.. code-block:: bash
export ANTHROPIC_API_KEY=sk-ant-... # Anthropic / Claude
export OPENAI_API_KEY=sk-... # OpenAI
export GOOGLE_API_KEY=... # Google Gemini
API keys can also be stored in a ``.env`` file in the working directory.
Overview
--------
The module exposes two high-level helper functions and a settings class:
.. list-table::
:header-rows: 1
:widths: 40 60
* - Symbol
- Purpose
* - :func:`~pyRadPlan.ai_agents.generate_beam_angles`
- Ask the LLM to suggest gantry (and couch) angles for a given treatment site and
radiation mode, then write them back into the :class:`~pyRadPlan.plan.Plan`.
* - :func:`~pyRadPlan.ai_agents.generate_voi_objectives`
- Ask the LLM to propose optimization objectives for each VOI in a
:class:`~pyRadPlan.cst.StructureSet` based on the treatment site and prescribed dose.
* - :class:`~pyRadPlan.ai_agents.AiSettings`
- Pydantic-settings class that reads the default model name from the environment
variable ``PYRADPLAN_AI_MODEL`` or a ``.env`` file.
Model selection
---------------
The default model is ``claude-sonnet-4-5``. You can override it globally via the
environment variable ``PYRADPLAN_AI_MODEL``, or per-call via the ``model=`` keyword:
.. code-block:: python
import os
os.environ["PYRADPLAN_AI_MODEL"] = "openai:gpt-4o-mini"
from pyRadPlan import ai_agents
# Check effective settings
print(ai_agents.AiSettings().model) # openai:gpt-4o-mini
# Override for a single call
pln = ai_agents.generate_beam_angles(pln, "prostate", model="gemini-2.0-flash")
pydantic-ai uses the provider prefix (``openai:``, ``anthropic:``, ``google-gla:``, …)
to select the backend automatically. When no prefix is given the model string is passed
to the Anthropic backend.
Generating beam angles
----------------------
:func:`~pyRadPlan.ai_agents.generate_beam_angles` populates ``pln.prop_stf`` with
``gantry_angles`` and matching ``couch_angles`` (all zeros by default):
.. code-block:: python
from pyRadPlan import IonPlan, ai_agents
pln = IonPlan(
radiation_mode="protons",
machine="Generic",
num_of_fractions=30,
prescribed_dose=60,
)
pln = ai_agents.generate_beam_angles(pln, treatment_site="prostate")
print(pln.prop_stf["gantry_angles"]) # e.g. [0.0, 90.0, 180.0]
An optional ``additional_context`` string lets you pass extra clinical constraints:
.. code-block:: python
pln = ai_agents.generate_beam_angles(
pln,
treatment_site="head and neck",
additional_context="Patient has a hip replacement on the right side.",
)
Generating optimization objectives
-----------------------------------
:func:`~pyRadPlan.ai_agents.generate_voi_objectives` iterates over the VOIs in a
:class:`~pyRadPlan.cst.StructureSet` and attaches LLM-suggested
:ref:`optimization objectives ` to each structure. By default any
previously assigned objectives are cleared first (``clear_existing=True``):
.. code-block:: python
from pyRadPlan import load_tg119, IonPlan, ai_agents
ct, cst = load_tg119()
pln = IonPlan(
radiation_mode="protons",
machine="Generic",
num_of_fractions=30,
prescribed_dose=60,
)
cst = ai_agents.generate_voi_objectives(pln, cst, treatment_site="prostate")
for voi in cst.vois:
if voi.objectives:
print(voi.name, [type(o).__name__ for o in voi.objectives])
The agent currently supports the following objective types:
``SquaredDeviation``, ``SquaredOverdosing``, ``SquaredUnderdosing``.
End-to-end example
------------------
The script ``examples/utils_ai_agents.py`` shows a complete workflow:
1. Load the TG119 phantom.
2. Use ``generate_beam_angles`` to obtain gantry angles for a prostate plan.
3. Use ``generate_voi_objectives`` to populate VOI objectives.
4. Run the standard dose calculation and fluence optimization with the AI-generated
settings.
5. Visualize the result.
.. code-block:: python
from dotenv import load_dotenv
from pyRadPlan import (
IonPlan, load_tg119, ai_agents,
generate_stf, calc_dose_influence, fluence_optimization,
plot_slice, DVHCollection,
)
load_dotenv() # load API key from .env
ct, cst = load_tg119()
pln = IonPlan(radiation_mode="protons", machine="Generic",
num_of_fractions=30, prescribed_dose=60)
pln.prop_opt = {"solver": "scipy"}
pln.prop_dose_calc = {"dose_grid": ct.grid}
pln = ai_agents.generate_beam_angles(pln, treatment_site="prostate")
cst = ai_agents.generate_voi_objectives(pln, cst, treatment_site="prostate")
stf = generate_stf(ct, cst, pln)
dij = calc_dose_influence(ct, cst, stf, pln)
fluence = fluence_optimization(ct, cst, stf, dij, pln)
result = dij.compute_result_ct_grid(fluence)
dvhs = DVHCollection.from_structure_set(cst, result["physical_dose"])
plot_slice(ct=ct, cst=cst, overlay=result["physical_dose"])
Settings reference
------------------
.. autoclass:: pyRadPlan.ai_agents.AiSettings
:members:
API reference
-------------
.. autofunction:: pyRadPlan.ai_agents.generate_beam_angles
.. autofunction:: pyRadPlan.ai_agents.generate_voi_objectives