Metadata-Version: 2.4
Name: fortel-agent
Version: 0.2.3
Summary: Run Fortel machine-learning jobs on your own machine.
Author: Fortel
License: Proprietary
Keywords: fortel,automl,machine-learning,runner
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Provides-Extra: torch
Requires-Dist: torch>=2.0; extra == "torch"
Requires-Dist: numpy>=1.24; extra == "torch"
Requires-Dist: pandas>=2.0; extra == "torch"
Requires-Dist: onnx>=1.16; extra == "torch"
Requires-Dist: onnxscript>=0.1; extra == "torch"

# fortel-agent

Runs Fortel's deep-learning jobs on your own hardware instead of ours. **This is
compute offload, not a privacy feature** — the dataset is sent from Fortel to
your machine to train on, and the trained model is sent back so it can be used
for predictions later. It exists because a GPU costs more per month than the
product does, not because anything is being kept from the server.

```bash
pip install "fortel-agent[torch]"           # omit [torch] for a connection test only
fortel-agent login --token fa_xxxxxxxx      # token from Fortel → Settings → Runners
fortel-agent start
```

Leave it running. Queue a job in Fortel and this picks it up.

## What comes back

Metrics, and the model. When a run finishes, the network is exported to **ONNX**
and uploaded, where it becomes a versioned entry in your Fortel model registry —
so a forecaster can be rolled further forward later, and a detector can score
rows that did not exist when it was trained.

ONNX rather than a pickle for a reason that matters on the receiving end:
unpickling *is* executing, so a server that unpickled whatever a runner uploaded
would be running code from a machine it does not control. ONNX is a graph
description that a runtime parses.

The upload is never load-bearing. No torch, a graph that will not export, a
model over the size ceiling, a dropped connection — each leaves your metrics
untouched and reports `model_saved: false` with the reason, rather than failing
the run.

## Why it exists

Deep-learning models don't fit the economics of a hosted plan — a GPU dyno costs
more per month than the whole product. Rather than not offering them, Fortel
sends an *instruction sheet* to a runner you control: a recipe name and bounded
parameters. Never code.

## Commands

| Command | What it does |
| --- | --- |
| `fortel-agent login --token …` | Saves and verifies a device token (`~/.fortel/agent.json`, mode 0600) |
| `fortel-agent start` | Claims and runs jobs until you stop it |
| `fortel-agent start --once` | Handles one job and exits — useful in CI or a notebook |
| `fortel-agent doctor` | Checks every link in the chain and names the broken one |

Configuration resolves command line → environment (`FORTEL_API`, `FORTEL_TOKEN`)
→ saved file.

## Running on Colab or Kaggle

The runner needs no inbound connectivity, so it works anywhere Python does —
including a free GPU notebook:

```python
!pip install fortel-agent
from fortel_agent import run_in_notebook
run_in_notebook(api="https://your-fortel-api", token="fa_xxxxxxxx")
```

Check your notebook provider's terms before using it as a backend for a hosted
service, and keep jobs short enough to finish inside one session — a runtime
that disconnects mid-job simply returns the work to the queue.

## Security

- **No inbound ports.** The runner only calls out, so it works behind home
  routers, university firewalls and corporate proxies.
- **Names, not code.** A job spec names a recipe from a fixed local registry.
  There is no `eval` and no import-by-name; an unrecognised name is refused even
  if the server sends it.
- **Bounded parameters.** The server clamps every hyperparameter before queuing,
  because this runs on your machine.
- **The token never crosses plain HTTP** except to loopback, for local
  development.
- **No standalone download links.** The job spec never contains a URL: the
  runner builds the dataset address from the server it is already
  authenticated against, so there is nothing to redirect it elsewhere and no
  link that works on its own if it leaks.
- **Revocation is immediate.** Revoking a device in Fortel stops it mid-job.

## Installation size

The core has **no third-party dependencies** — the transport is standard library
only, so the install cannot fail on a dependency resolution. Recipes needing a
heavy stack are extras:

```bash
pip install "fortel-agent[torch]"
```

## Adding a recipe

One file in `fortel_agent/recipes/`, one line in its `REGISTRY`. Transport,
retries, heartbeating, progress reporting and failure handling are shared.

```python
def run(params: dict, report, dataset=None, artifact=None) -> dict:
    frame = pd.read_csv(dataset)          # None when the recipe needs no data
    for step in range(1, params["epochs"] + 1):
        ...
        report(progress=step, total=params["epochs"], message=f"epoch {step}")
    return {"final_loss": loss}
```

`dataset` is a path inside a per-job directory the runner creates and deletes
when the job ends, so the copy sent for training does not outlive the job here.
`artifact` is the optional sink for handing the trained model back — see
`recipes/export.py`; a recipe that trains nothing simply ignores it.

Give it an `is_available()` attribute if it needs an optional dependency — a
recipe that can't run must not be advertised, or the server will hand it work
that fails on someone else's schedule.
