Tools¶
The functions behind the agent's tool calls. See Agent tools for what each one is for.
Results¶
helioai.tools.results ¶
What a tool call returns, typed once at the registry boundary.
Every tool hands back a dict (or, for the remote MCP proxies and the internal tools, a
string), and the registry used to serialise it to JSON on the spot. Downstream, five
readers — the history writer, the artifact extractor, the figure review, the MCP server's
isError, the event display — each parsed that string back and sniffed its shape. The
ToolResult carries the parsed payload alongside the exact text the model receives, so
the shape is read once and the text is produced once.
for_llm() is the contract that lets this module exist without changing agent
behaviour: it returns, byte for byte, the string registry.call_tool used to return —
a string result untouched, a dict as json.dumps(..., ensure_ascii=False, default=str),
a failure as {"error": ...}. tests/test_tool_results.py holds it to captured results.
ToolResult
dataclass
¶
The outcome of one tool call: the parsed payload and the text the model sees.
Attributes:
| Name | Type | Description |
|---|---|---|
tool |
str
|
Name of the tool that produced it. |
payload |
Any
|
What the tool returned — a dict for every shipped tool, a list or a
scalar in principle, a string for a remote MCP proxy whose text is not
JSON. A string that is JSON (the internal tools, the |
raw |
str | None
|
The exact string a tool returned, when it returned one. |
Source code in helioai/tools/results.py
ok
property
¶
False when the payload carries a truthy error, which is how every shipped
tool — and the registry itself — reports a failure.
from_raw
classmethod
¶
Wrap whatever a tool returned.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tool
|
str
|
The tool's name. |
required |
obj
|
Any
|
Its return value — dict, list, string, anything JSON can carry. |
required |
Returns:
| Type | Description |
|---|---|
ToolResult
|
A result whose |
ToolResult
|
whose |
Source code in helioai/tools/results.py
failure
classmethod
¶
A result for a tool that never ran, or raised.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tool
|
str
|
The tool that was asked for. |
required |
message
|
str
|
Why it failed. An empty message is replaced by the word |
required |
Returns:
| Type | Description |
|---|---|
ToolResult
|
A result whose |
ToolResult
|
always returned for these cases, ASCII-escaped as before. |
Source code in helioai/tools/results.py
with_payload ¶
The same call with an amended payload — what the figure review returns.
The original text is dropped on purpose: for_llm() must describe the payload
the model is about to read, amendments included.
Source code in helioai/tools/results.py
for_llm ¶
The text appended to the history and shown to the model.
Returns:
| Type | Description |
|---|---|
str
|
The tool's own string when it returned one; otherwise the payload as JSON |
str
|
with non-ASCII kept and unknown types stringified — the serialisation |
str
|
|
Source code in helioai/tools/results.py
Parameter search¶
helioai.tools.rag ¶
ChromaDB semantic search over the speasy catalog.
The index is built by indexer.py (run: helioai index).
This module provides the read-only search path used at agent query time.
Models load lazily and are cached at module scope.
search ¶
search(query: str, top_k: int = 5, *, provider: str | None = None, region: str | None = None, measurement_type: str | None = None, window: tuple[str, str] | None = None) -> list[dict]
Semantic search over speasy catalog (single query).
Optional metadata filters narrow the search at query time (the metadata is
already indexed by indexer.py): provider (amda/cda/csa/ssc) is the most
useful — it counters CDA's dominance of the catalog.
With hybrid search enabled (default), a BM25 lexical channel is fused with
the dense channel via RRF — this recovers exact id/code matches (e.g.
BGSEc) that dense embeddings miss.
The returned ORDER is the ranking. score is only the fusion or cross-encoder
value, frozen before the domain rerank reorders the list, so it does not decrease
monotonically down the results and must not be re-sorted on — reading it as a
ranking puts the demoted product back on top. It is kept here to tune the
retrieval; search_parameters drops it before a model sees it.
For several queries at once use search_batch (one embedding pass + one
Chroma call). Returns a list of dicts: {id, name, description, coverage, score}.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
Free-text English description of ONE parameter. |
required |
top_k
|
int
|
Number of results. |
5
|
provider
|
str | None
|
Restrict to one provider (amda/cda/csa/ssc). |
None
|
region
|
str | None
|
SPASE region filter (exact indexed string). |
None
|
measurement_type
|
str | None
|
Measurement-type filter (exact indexed string). |
None
|
window
|
tuple[str, str] | None
|
|
None
|
Example
search("ACE solar wind proton density", top_k=2)[0] {'id': 'cda/AC_H2_SWE/Np', 'name': 'Proton No. density', 'description': 'Proton No. density. Solar Wind Proton Number Density, ' 'scalar. ACE/SWEPAM ... Units: #/cc. ...', 'score': 1.0, ...}
Source code in helioai/tools/rag.py
search_batch ¶
search_batch(queries: list[str], top_k: int = 5, *, provider: str | None = None, region: str | None = None, measurement_type: str | None = None, window: tuple[str, str] | None = None) -> list[list[dict]]
Resolve several queries in ONE pass — the 'composed RAG'.
Encodes all queries in a single embedding pass and issues a single multi-vector ChromaDB query (Chroma returns one result set per query natively), then fuses each independently. Returns one result list per input query, aligned by index (blank queries map to []).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
queries
|
list[str]
|
One free-text query per parameter to resolve. |
required |
top_k
|
int
|
Results per query. |
5
|
provider
|
str | None
|
Same filter as |
None
|
region
|
str | None
|
Same filter as |
None
|
measurement_type
|
str | None
|
Same filter as |
None
|
window
|
tuple[str, str] | None
|
|
None
|
Example
ace, wind = search_batch(["ACE solar wind proton density", ... "Wind magnetic field GSE"], top_k=3) ace[0].get("id"), wind[0].get("id") ('cda/AC_H2_SWE/Np', 'cda/WI_H0_MFI/BGSEa')
Source code in helioai/tools/rag.py
862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 | |
search_catalogs ¶
Semantic search over the AMDA catalog/timetable index.
Requires helioai index to have been run at least once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
Free text describing the events wanted. |
required |
top_k
|
int
|
How many products to return. |
5
|
product_type
|
str | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
list[dict]
|
Dicts of |
list[dict]
|
first. Empty when the catalog collection is absent — a missing index is |
list[dict]
|
reported as no results rather than as a crash, because the agent can |
list[dict]
|
still answer from parameters. |
Source code in helioai/tools/rag.py
990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 | |
Data access¶
helioai.tools.speasy_tools ¶
speasy tools: data access layer wrapping the speasy library.
speasy provides unified access to 70+ missions and 65k+ products from CDAWeb, AMDA, CSA, SSC and others. These tools are the helioai equivalent of AMDA's download_timeseries and list_parameters.
get_timeseries
async
¶
get_timeseries(param_id: str, start: str, stop: str, max_points: int = 5000, _data_dir: str | None = None) -> dict
Download a time series from any speasy provider.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
param_id
|
str
|
speasy parameter id (e.g. 'amda/imf', 'cdaweb/AC_H0_MFI/BGSEc') |
required |
start
|
str
|
ISO 8601 start time (e.g. '2024-01-01T00:00:00') |
required |
stop
|
str
|
ISO 8601 stop time |
required |
max_points
|
int
|
max samples to return (downsampled if needed) |
5000
|
_data_dir
|
str | None
|
injected by the runtime ( |
None
|
The param_id should be in speasy format: "{provider}/{xmlid}" e.g. "amda/ace_epam_ca60_he", "cda/ACE_H0_MFI/BGSEc" (returned by search_parameters).
Returns dict with: param_id, start, stop, units, shape, n_points, preview (first 10 rows as CSV)
Example
await get_timeseries("amda/imf", "2010-01-01T00:00:00", "2010-01-01T06:00:00") {'dataset': 'imf', 'param_id': 'amda/imf', 'units': 'nT', 'components': ['bx', 'by', 'bz'], 'cadence': '16 s', 'shape': [1350, 3], 'n_points': 1350, 'n_valid': 1350, 'quality': {'missing_pct': 0.0, ...}, 'preview': '2010-01-01T00:00:09.000000000 -1.839, 2.308, 0.108\n...', ...}
Source code in helioai/tools/speasy_tools.py
list_missions
async
¶
List available speasy data providers and their top-level missions.
Returns a summary dict with provider names and approximate product counts.
Example
await list_missions() {'providers': ['amda', 'archive', 'cda', 'csa', 'ssc', 'uiowaephtool'], 'note': 'Use search_parameters to find specific parameters. ...'}
Source code in helioai/tools/speasy_tools.py
search_parameters
async
¶
search_parameters(query: str | None = None, top_k: int = 5, provider: str | None = None, queries: list[str] | None = None, start: str | None = None, stop: str | None = None) -> dict
Semantic search over the speasy catalog (83k+ products).
Requires the index to be built first (run: helioai index). Falls back to a direct speasy text match if no index is found.
Every result carries the product's published coverage. Passing the window you
intend to download sorts products that cannot cover it to the bottom and flags them,
which is cheaper than discovering it one failed download at a time — resolving a
2015 ephemeris used to cost four turns against products that stop in 1997.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str | None
|
free-text English query for a SINGLE parameter. |
None
|
queries
|
list[str] | None
|
list of queries to resolve SEVERAL parameters in ONE call (preferred when 2+ parameters are needed — much cheaper). |
None
|
top_k
|
int
|
number of results per query. |
5
|
provider
|
str | None
|
optional — restrict to one provider (amda/cda/csa/ssc). |
None
|
start
|
str | None
|
optional ISO start of the interval you intend to download. |
None
|
stop
|
str | None
|
optional ISO stop. Both are needed for the filter to apply. |
None
|
Returns either {query, provider, results} (single) or {provider, groups: [{query, results}]} (batch).
Example
await search_parameters(query="ACE solar wind proton density", top_k=3) {'query': 'ACE solar wind proton density', 'provider': None, 'results': [ {'id': 'cda/AC_H2_SWE/Np', 'description': 'Proton No. density. Solar Wind Proton Number Density, scalar. ' 'ACE/SWEPAM ... 1-Hour Level 2 Data ... Units: #/cc. ...', 'coverage': '1998-02-04 → 2024-07-09'}, ...]}
Source code in helioai/tools/speasy_tools.py
Event catalogs¶
helioai.tools.catalog_tools ¶
Catalog and timetable tools for HelioAI.
Exposes the 29 CatalogIndex + 188 TimetableIndex from the AMDA speasy inventory as first-class agent tools. The key capability is get_events_timeseries: download a parameter for every event in a catalog in one speasy call, opening the door to superposed epoch analysis.
list_catalogs
async
¶
List available AMDA event catalogs and timetables.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
type
|
str
|
'catalog', 'timetable', or 'all' (default). |
'all'
|
region
|
str | None
|
optional keyword filter on name/description (e.g. 'ICME', 'bow shock', 'MMS'). |
None
|
query
|
str | None
|
optional free-text description of the events wanted; the list is then ordered by semantic relevance to it instead of by size. |
None
|
Returns a list of entries with id, name, type, nb_events, survey range and description.
Use the id field with get_catalog() and get_events_timeseries().
Example
await list_catalogs(type="catalog", region="ICME") {'total': 3, 'type_filter': 'catalog', 'region_filter': 'ICME', 'catalogs': [ {'id': 'amda/sharedcatalog_41', 'name': 'ICME_multi-catalog', 'type': 'catalog', 'nb_events': 2003, 'survey_start': '1975-01-08', 'survey_stop': '2022-10-21', 'description': '...'}, ...]}
Source code in helioai/tools/catalog_tools.py
get_catalog
async
¶
get_catalog(catalog_id: str, start: str | None = None, stop: str | None = None, max_events: int = 10, columns: list[str] | None = None, where: dict | None = None, sort_by: str | None = None, descending: bool = False, offset: int = 0) -> dict
Download and summarize an AMDA event catalog or timetable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog_id
|
str
|
speasy uid from list_catalogs (e.g. 'amda/sharedcatalog_41'). |
required |
start
|
str | None
|
optional ISO 8601 start — filter events beginning after this time. |
None
|
stop
|
str | None
|
optional ISO 8601 stop — filter events beginning before this time. |
None
|
max_events
|
int
|
maximum events to include in the sample (default 10). |
10
|
columns
|
list[str] | None
|
restrict the metadata columns returned per event. |
None
|
where
|
dict | None
|
server-side row filter — {"column": str, "op": "eq|ne|gt|gte|lt|lte|contains", "value": any}. |
None
|
sort_by
|
str | None
|
column name to sort events by before slicing. |
None
|
descending
|
bool
|
sort direction (default ascending). |
False
|
offset
|
int
|
pagination offset into the filtered+sorted events. |
0
|
Returns catalog metadata + a sample of events (start, stop, key columns). Use get_events_timeseries() to download a parameter over all events.
Example
await get_catalog("amda/sharedcatalog_41", start="2015-01-01", stop="2016-01-01", ... max_events=5, sort_by="start") {'_kind': 'catalog_preview', 'catalog_id': 'amda/sharedcatalog_41', 'name': 'ICME_multi-catalog', 'nb_events_total': 2003, 'nb_events_filtered': ..., 'returned': 5, 'columns': [...], 'sample': [{'start': ..., 'stop': ..., ...}, ...], 'survey_start': '1975-01-08', 'survey_stop': '2022-10-21'}
Source code in helioai/tools/catalog_tools.py
get_events_timeseries
async
¶
get_events_timeseries(catalog_id: str, param_id: str, start: str, stop: str, max_events: int = 50, _data_dir: str | None = None) -> dict
Download a parameter for every event in a catalog window (superposed epoch).
This is the core catalog tool: it fetches N time series in a SINGLE speasy call using the native multi-interval API. Use it for: - Superposed epoch analysis (stack-plot across events) - Statistical summaries per event (min/max/mean) - Comparing a parameter across e.g. all ICME crossings in a year
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog_id
|
str
|
speasy uid from list_catalogs (e.g. 'amda/sharedcatalog_41'). |
required |
param_id
|
str
|
speasy parameter id (e.g. 'amda/imf_gsm') — resolve via search_parameters first. |
required |
start
|
str
|
ISO 8601 start — restrict to events beginning after this time. |
required |
stop
|
str
|
ISO 8601 stop — restrict to events beginning before this time. |
required |
max_events
|
int
|
cap on events to download (default 20 — each is one speasy call slot). |
50
|
_data_dir
|
str | None
|
injected by the runtime ( |
None
|
Returns per-event statistics and saves the raw data to the workspace for run_python.
Example
await get_events_timeseries("amda/sharedcatalog_41", "amda/imf", ... "2015-01-01", "2016-01-01", max_events=10) {'catalog_id': 'amda/sharedcatalog_41', 'param_id': 'amda/imf', 'stats': [ {'event': 0, 'start': '2015-01-03T...', 'stop': '2015-01-04T...', 'n_points': ..., ...}, ...], ...}
Source code in helioai/tools/catalog_tools.py
save_catalog
async
¶
save_catalog(name: str, events: list[dict], description: str = '', _catalogs_dir: str | None = None) -> dict
Save a list of events as a local catalog under the local/
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Catalog name — lowercase letters, digits, hyphens, underscores (1-40 chars). |
required |
events
|
list[dict]
|
List of dicts with 'start' and 'stop' ISO 8601 strings plus optional extra keys. |
required |
description
|
str
|
Short description (optional). |
''
|
_catalogs_dir
|
str | None
|
injected by the runtime ( |
None
|
Returns {"catalog_id": "local/
Example
await save_catalog("my-shocks", ... [{"start": "2015-03-17T04:01:00", "stop": "2015-03-17T05:00:00", ... "note": "St. Patrick's Day storm shock"}])
Source code in helioai/tools/catalog_tools.py
Plasma physics¶
helioai.tools.plasmapy_tools ¶
PlasmaPy-based plasma physics calculations exposed as agent tools.
Each function accepts plain SI-ish numbers (nT, cm⁻³, eV) and returns a dict with value, unit, and a brief physical context — ready for LLM consumption.
SPASE ParticleQuantity / FieldQuantity mapping: plasma_beta → PlasmaBeta (ActivityIndex) gyrofrequency → Gyrofrequency (FieldQuantity + ParticleQuantity) debye_length → (ParticleQuantity implied) alfven_speed → AlfvenVelocity (ParticleQuantity) inertial_length → (ParticleQuantity implied) power_spectrum → Spectrum (MeasurementType)
plasma_beta
async
¶
Compute plasma beta — ratio of thermal pressure to magnetic pressure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
B_nT
|
float
|
Magnetic field magnitude in nT |
required |
n_cm3
|
float
|
Number density in cm⁻³ |
required |
T_eV
|
float
|
Temperature in eV |
required |
Returns dict with beta (dimensionless) and regime interpretation.
Example
await plasma_beta(B_nT=5.0, n_cm3=10.0, T_eV=20.0) {'beta': 3.221367, 'unit': 'dimensionless', 'regime': 'high-β plasma (β ~ 1-10) — typical magnetosheath / plasma sheet', ...}
Source code in helioai/tools/plasmapy_tools.py
gyrofrequency
async
¶
Compute particle gyrofrequency (cyclotron frequency).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
B_nT
|
float
|
Magnetic field magnitude in nT |
required |
particle
|
str
|
'proton', 'electron', 'alpha' (default: proton) |
'proton'
|
Returns dict with frequency in Hz and angular frequency in rad/s.
Example
await gyrofrequency(B_nT=5.0) {'frequency_Hz': 0.0762, 'angular_frequency_rad_s': 0.4789, 'particle': 'proton', 'B_nT': 5.0, 'period_s': 13.118895} await gyrofrequency(B_nT=5.0, particle="electron")
Source code in helioai/tools/plasmapy_tools.py
debye_length
async
¶
Compute electron Debye length.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_cm3
|
float
|
Electron number density in cm⁻³ |
required |
T_eV
|
float
|
Electron temperature in eV |
required |
Returns dict with Debye length in km and meters.
Example
await debye_length(n_cm3=10.0, T_eV=12.0) {'debye_length_m': 8.143475, 'debye_length_km': 0.008143475, 'inputs': {'n_cm3': 10.0, 'T_eV': 12.0}}
Source code in helioai/tools/plasmapy_tools.py
alfven_speed
async
¶
Compute Alfvén speed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
B_nT
|
float
|
Magnetic field magnitude in nT |
required |
n_cm3
|
float
|
Ion number density in cm⁻³ |
required |
mass_amu
|
float
|
Ion mass in atomic mass units (default 1.0 = proton) |
1.0
|
Returns dict with Alfvén speed in km/s.
Example
await alfven_speed(B_nT=5.0, n_cm3=5.0) {'alfven_speed_km_s': 48.937, 'alfven_speed_m_s': 48936.9, 'inputs': {'B_nT': 5.0, 'n_cm3': 5.0, 'mass_amu': 1.0}, ...}
Source code in helioai/tools/plasmapy_tools.py
inertial_length
async
¶
Compute ion or electron inertial length (skin depth).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_cm3
|
float
|
Number density in cm⁻³ |
required |
particle
|
str
|
'proton' or 'electron' (default: proton) |
'proton'
|
Returns dict with inertial length in km and meters.
Example
await inertial_length(n_cm3=5.0) {'inertial_length_km': 101.8354, 'inertial_length_m': 101835.35, 'particle': 'proton', 'inputs': {'n_cm3': 5.0}}
Source code in helioai/tools/plasmapy_tools.py
power_spectrum
async
¶
Compute power spectral density using Welch's method.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
list[float]
|
Time series as a list of floats |
required |
dt_s
|
float
|
Sampling interval in seconds |
required |
nperseg
|
int | None
|
Samples per FFT segment (default: min(256, len(values)//4)) |
None
|
Returns dict with frequencies (Hz), PSD values, peak frequency, and export-ready summary for LLM interpretation.
Example
import math wave = [math.sin(2 * math.pi * 0.1 * i) for i in range(512)] psd = await power_spectrum(wave, dt_s=1.0) psd["peak_frequency_Hz"], psd["peak_period_s"] (0.101562, 9.846)
Source code in helioai/tools/plasmapy_tools.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 | |
Sandbox¶
helioai.tools.sandbox ¶
Python sandbox: execute user/LLM-generated code in an isolated subprocess.
Security model
- Runs in a fresh subprocess (separate memory, no shared globals)
- Hard timeout (default 30s) — kills the process if exceeded
- stdout/stderr captured and returned
- Host credentials (~/.ssh, ~/.gnupg, ~/.config/gh, etc.) and .env masked under bubblewrap
- No network isolation (speasy needs network access) — trust LLM-generated code
Pre-imports available in sandbox: speasy, plasmapy, numpy, scipy, matplotlib, astropy Figures are saved to a temp directory; paths are returned (not base64). Use export(name, array) to share numerical data with the LLM.
run_python
async
¶
run_python(code: str, timeout: float = 60.0, _plot_dir: str | None = None, _run_idx: int | None = None, _no_net: bool = False) -> dict
Execute Python code in an isolated subprocess.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
code
|
str
|
Python source code to execute. Has access to speasy (spz), plasmapy (pf), numpy (np), scipy, matplotlib (Agg — plt.show() saves to disk), astropy units (u). Call export(name, array) to share numerical results with the LLM. |
required |
timeout
|
float
|
maximum execution time in seconds — clamped to _MAX_TIMEOUT_S |
60.0
|
_plot_dir
|
str | None
|
injected by the agent loop — workspace dir for this run. Not exposed in the LLM tool schema. |
None
|
Returns dict with
- stdout: captured text output
- stderr: captured errors/warnings
- figure_paths: list of absolute paths to saved PNG files
- exports: dict of named numerical summaries (from export() calls)
- error: error message if execution failed
Example
await run_python( ... "import numpy as np\n" ... "export('rms', np.sqrt(np.mean(np.arange(8) ** 2)))\n" ... "print('done')" ... ) {'stdout': 'done', 'exports': {'rms': {'mean': 4.1833..., 'min': 4.1833..., 'max': 4.1833..., 'std': 0.0, 'n_finite': 1, 'n_nan': 0, ...}}, 'figure_paths': [], 'error': None, ...}
Source code in helioai/tools/sandbox.py
846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 | |
Sandbox helpers¶
Available inside run_python, and re-emitted into exported notebooks.
helioai.tools.sandbox_helpers ¶
Standalone physics helpers importable inside the sandbox.
MUST NOT import anything from helioai.* — the sandbox masks .env and strips the environment, so helioai.config would fail fast at import time.
Boundary models are clean-room implementations from the published papers: - Shue et al. (1998), JGR 103, 17691, doi:10.1029/98JA01103 - Jelinek et al. (2012), JGR 117, A05208, doi:10.1029/2011JA017252 Coordinate transforms wrap geopack (MIT, Tsyganenko models port).
transform_coords ¶
Rotate vectors between geocentric frames: gse, gsm, sm, geo, mag, gei.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
time
|
Any
|
ISO string(s), datetime(s), numpy datetime64, or epoch seconds, all UTC. One timestamp per vector, or a single one for all. |
required |
vectors
|
Any
|
Shape |
required |
frm
|
str
|
Source frame — gse, gsm, sm, geo, mag or gei. |
'gse'
|
to
|
str
|
Target frame, same set. |
'gsm'
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The rotated vectors, same shape as the input. |
The dipole tilt is recomputed per point (geopack.recalc), which is exact
but linear in N — comfortable to ~1e4 points, not to a full mission.
Example
t = np.array(["2015-03-17T04:00:00"], dtype="datetime64[s]") transform_coords(t, np.array([[10.0, 0.0, 0.0]]), "gse", "gsm") array([[10., 0., 0.]]) # the X axis is shared by GSE and GSM
Source code in helioai/tools/sandbox_helpers.py
mp_shue1998 ¶
Shue et al. (1998) magnetopause: r = r0 * (2 / (1 + cos(theta)))**alpha.
r0 = (10.22 + 1.29tanh(0.184(Bz + 8.14))) * Pdyn(-1/6.6) alpha = (0.58 - 0.007Bz) * (1 + 0.024ln(Pdyn)) Args: pdyn_nPa: Solar wind dynamic pressure, nPa. bz_nT: IMF Bz in GSM, nT. Southward (negative) erodes the standoff. theta_deg: Angles from the Earth-Sun line. Defaults to 0..170 deg; beyond that the model is extrapolated past where it was fitted.
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray]
|
|
Reference: Shue et al. (1998), JGR 103, 17691, doi:10.1029/98JA01103.
Example
theta, r = mp_shue1998(2.0, -5.0) # Pdyn=2 nPa, Bz=-5 nT round(float(r[0]), 2), round(float(r[90]), 2) (9.81, 15.13) # standoff and flank, in R_E
Source code in helioai/tools/sandbox_helpers.py
bs_jelinek2012 ¶
Jelinek et al. (2012) bow shock: parabola rho^2 = 4R(R - x) / lam^2.
R = 15.02 * Pdyn**(-1/6.55) is the subsolar standoff in R_E, lam = 1.17.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pdyn_nPa
|
float
|
Solar wind dynamic pressure, nPa. |
required |
theta_deg
|
Any
|
Angles from the Earth-Sun line. Defaults to 0..120 deg. |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
|
ndarray
|
a clipped value that would plot as a real boundary. |
Reference: Jelinek et al. (2012), JGR 117, A05208, doi:10.1029/2011JA017252.
Example
theta, r = bs_jelinek2012(2.0) # Pdyn=2 nPa round(float(r[0]), 2) 13.51 # subsolar standoff, in R_E
Source code in helioai/tools/sandbox_helpers.py
Recipes¶
helioai.tools.recipes ¶
Derived-recipe tools — list, load and run scientific Python recipes.
Recipes live in data/recipes/ as .py files with a YAML comment header: # name: theta_bn # description: Compute the shock normal angle theta_Bn from upstream/downstream B. # inputs: B_up (nT vec), B_dn (nT vec) # outputs: theta_bn_deg
list_recipes() returns the catalogue; load_recipe(name) returns the source code; run_recipe(name, inputs) executes it verbatim in the sandbox on the caller's inputs.
list_recipes
async
¶
List all available derived recipes with their name, description and the call that runs them.
Each entry carries run_with, the run_recipe(...) call with the recipe's own input
names or functions, so a model can go from the catalogue straight to running one:
loading a recipe first cost one LLM call per recipe, and the call is where a session
pays — every one re-sends the whole context.
Returns dict with 'recipes' list (sorted by name). Each entry has 'name', 'description', 'inputs', 'outputs' (when present in header) and 'run_with'. Returns {"recipes": []} when the recipes directory does not exist.
Example
await list_recipes() {'recipes': [{'name': 'fill_values', 'description': '...', 'run_with': '...'}, {'name': 'mvab', ...}, {'name': 'rankine_hugoniot', ...}, ...]}
Source code in helioai/tools/recipes.py
load_recipe
async
¶
Load the source code of a named recipe.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Recipe name without .py extension (e.g. 'theta_bn'). |
required |
Returns dict with 'name' and 'code'. Returns {'error': ...} when not found or when the name contains path-traversal characters.
Example
await load_recipe("theta_bn")
Source code in helioai/tools/recipes.py
run_with ¶
The one line that runs a recipe as shipped, read off its own source.
A model that has just read a recipe's code is one paste away from running a copy of
it in run_python — which is how a 54.85° θ_Bn came out of a 12-minute window the
recipe would never have chosen. The line names the tool and, exactly, what to bind.
A recipe declares its usual call in its header (# run:), because the names it reads
are not a call: rankine_hugoniot reads eighteen, in three alternative bindings,
and listed flat, alphabetically, they do not say which to bind. The other names it
reads follow it. Without a declaration the line is derived: the
variables read with globals().get for a script, the public functions for a library.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The recipe. |
required |
code
|
str
|
Its source. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The declared call, or a |
str
|
names or functions. |
Source code in helioai/tools/recipes.py
recipe_script ¶
Assemble the script run_recipe executes: inputs, the recipe, the call.
The recipe's source is inserted verbatim — not a function copied out of it, not a
formula rewritten from memory — so its calibrated constants, its checks and its own
export() calls run as shipped. __name__ is set first so a recipe that guards a
demo behind if __name__ == "__main__": runs its functions and not its demo, the way
it would if imported. The call comes last, for the recipes that are a library of
functions rather than a script.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The recipe. |
required |
code
|
str
|
Its source, as |
required |
inputs
|
dict
|
|
required |
call
|
str | None
|
An expression evaluated after it, or None. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The Python source, as it is written to the session's |
Source code in helioai/tools/recipes.py
run_recipe
async
¶
run_recipe(name: str, inputs: dict | None = None, call: str | None = None, timeout: float = 60.0, _plot_dir: str | None = None, _run_idx: int | None = None, _no_net: bool = False) -> dict
Run a shipped recipe on the session's data, without the model rewriting it.
load_recipe hands the model the source, and the model then pastes a part of it
into run_python — or reads the constants and rewrites the computation by hand,
which is what the recipe check keeps catching. Here the recipe runs as shipped: the
inputs are bound first, the source follows verbatim, and an optional call applies
one of its functions. The exports are the recipe's own, the script written to the
workspace is the one the export reproduces, and the run is recorded as a use of the
recipe (method_used) with its reference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Recipe name, as |
required |
inputs
|
dict | None
|
|
None
|
call
|
str | None
|
An expression evaluated after the recipe, for a recipe that is a library
of functions — |
None
|
timeout
|
float
|
Sandbox time budget in seconds. |
60.0
|
_plot_dir
|
str | None
|
Session workspace, injected by the runtime. |
None
|
_run_idx
|
int | None
|
Script index in that workspace, injected by the runtime. |
None
|
_no_net
|
bool
|
Deny the sandbox a network namespace, injected by the runtime. |
False
|
Returns:
| Type | Description |
|---|---|
dict
|
The |
dict
|
|
dict
|
|
dict
|
the recipes read their inputs with |
dict
|
when a name is bound wrong — it also carries |
dict
|
usage, public signatures and |
dict
|
|
dict
|
that is not a Python name (with the notice). |
Source code in helioai/tools/recipes.py
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 | |
Literature¶
helioai.tools.literature ¶
NASA ADS literature search — find_papers tool.
find_papers
async
¶
find_papers(query: str, max_results: int = 5, year_start: int | None = None, year_end: int | None = None, sort: str = 'relevance', _transport: AsyncBaseTransport | None = None) -> dict
Search NASA ADS for papers relevant to an event, parameter or method.
Requires ADS_API_TOKEN; without it the tool returns an error rather than
raising, so the agent can tell the user what is missing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
Free-text ADS query — event, parameter, method or author. |
required |
max_results
|
int
|
How many papers to return. |
5
|
year_start
|
int | None
|
Earliest publication year, inclusive. None leaves it open. |
None
|
year_end
|
int | None
|
Latest publication year, inclusive. None leaves it open. |
None
|
sort
|
str
|
|
'relevance'
|
_transport
|
AsyncBaseTransport | None
|
Test seam for injecting an httpx transport. Not part of the tool schema the model sees. |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
|
dict
|
year, bibcode, doi, citations and abstract. On a missing token or an ADS |
dict
|
failure, an |
dict
|
has to be able to report the cause. |
Example
await find_papers("interplanetary shock Rankine-Hugoniot multi-spacecraft", ... max_results=2) {'query': '...', 'papers': [ {'title': 'Multiple spacecraft observations of interplanetary shocks: ...', 'authors': 'Russell, C. T. et al.', 'year': '1983', 'bibcode': '1983JGR....88.9941R', 'doi': '10.1029/JA088iA12p09941', 'citations': 72, 'abstract': '...'}, ...], 'note': '...'}
Source code in helioai/tools/literature.py
Registry¶
helioai.tools.registry ¶
Tool registry: wraps Python async functions with JSON Schema metadata.
The agent loop calls registry.call_tool(name, args) and never imports
tool modules directly, which keeps the dependency surface small and makes
sub-agent tool whitelisting trivial. Every call comes back as a ToolResult
(tools/results.py): the payload once, the model's text once.
Tool
dataclass
¶
A registered tool: an async function plus the schema shown to the model.
Source code in helioai/tools/registry.py
ToolRegistry ¶
Maps tool names to async functions and their JSON Schemas.
The agent loop dispatches through here and never imports tool modules, which keeps the dependency surface small and makes per-role whitelisting trivial.
Source code in helioai/tools/registry.py
register ¶
Decorator that registers an async function as a tool.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
read_only
|
bool
|
False for a tool that changes something a user would care about — running arbitrary code, writing a catalogue. MCP clients gate auto-approval on it, so the default is the safe-to-repeat majority and the two exceptions say so at their registration site. Kept here rather than in a table beside the MCP server: the registry is the one place that already describes every tool. |
True
|
Source code in helioai/tools/registry.py
list_tool_defs ¶
Return tool definitions for the model.
Args: only: Restrict to these names — how sub-agent whitelists are applied. None returns every registered tool.
Source code in helioai/tools/registry.py
call_tool
async
¶
Invoke a tool and return its result, typed.
Never raises: an unknown tool, a rejected private argument and any exception
the tool lets out all come back as a failed ToolResult, whose for_llm() is
the {"error": ...} line this method used to return as a string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Registered tool name. |
required |
arguments
|
dict | None
|
Caller-supplied (LLM/MCP) arguments; may not carry private |
required |
trusted
|
dict | None
|
Framework-injected arguments (the sandbox output directory) that bypass that guard. |
None
|
Returns:
| Type | Description |
|---|---|
ToolResult
|
The tool's payload and the exact text the model will read. |
Source code in helioai/tools/registry.py
is_read_only ¶
Whether the tool leaves the user's world unchanged. Unknown names count as not.
MCP client¶
helioai.tools.mcp_client ¶
Generic MCP client — mounts remote MCP server tools into the ToolRegistry.
Configured via HELIOAI_MCP_SERVERS, a JSON object keyed by server alias: {"ads": {"url": "https://.../mcp", "headers": {"Authorization": "Bearer x"}}, "alphaxiv": {"command": "npx", "args": ["-y", "mcp-remote", "https://api.alphaxiv.org/mcp/v1"]}}
Remote tools are registered as "
discover_and_register
async
¶
Connect to every configured MCP server and register its tools.
Idempotent: only the first call does any work. Servers that are unreachable are logged and skipped rather than fatal — a dead remote must not stop HelioAI from starting.
Returns:
| Type | Description |
|---|---|
list[str]
|
The names under which remote tools were registered, prefixed by alias. |