---
title: "AI Agent Capabilities"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{ai-agent}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

## Why an AI agent in *finnts*?

The AI agent is a **tool-calling orchestration layer** that sits on top of the core *finnts* pipeline. It uses an LLM to:

- run lightweight EDA (missing data, outliers, (P)ACF, seasonality, stationarity),
- select/adjust **feature recipes** (lags, rolling windows, Fourier, date feats),
- choose **model families** (local vs global; ARIMA/ETS/LM/Tree/GBM/NN, etc.), backtests, and ensembling,
- optionally **detect and use hierarchies** (bottoms-up, standard, grouped),
- generate a **reproducible best run** and a consolidated **forecast artifact** you can pull with a single call.

You keep control through a few inputs (data, horizon, optional regressors, performance goal, iteration budget); the agent does the rest.

Core Agent Functions:

- `iterate_forecast()`: Run the agent to iterate toward a best forecast run.
- `update_forecast()`: Update forecasts with new data, using models trained in previous agent runs, optionally re-invoking the agent if accuracy degrades.
- `ask_agent()`: Ask natural language questions about your forecast results and get data-driven answers.

---

## What the agent produces

- A versioned **agent run** (with `agent_version` and `run_id`) and log files.
- A **best-run table** per time series with metrics.
- A combined **forecast table** (optionally reconciled if hierarchy is used).
- EDA artifacts (missing data, outliers, stationarity, seasonality, ACF/PACF).
- Summaries of trained models (hyperparameters, recipes, feature importance).

Use these helpers to retrieve outputs:

- `get_best_agent_run(agent_info)`
- `get_agent_forecast(agent_info)`
- `ask_agent(agent_info, question)`

---

## Prerequisites

- Package: `finnts`  
- LLM driver: the agent uses an **ellmer** Chat object for tool calling.
  - Example here shows Azure OpenAI, but any ellmer Chat backend works.

Set up environment variables for Azure OpenAI (example):

```{r eval=FALSE}
Sys.setenv(
  AZURE_OPENAI_ENDPOINT    = "<your-endpoint>",
  AZURE_OPENAI_API_KEY     = "<your-key>",
  AZURE_OPENAI_API_VERSION = "<api-version>"
)
```

- TimeGPT Setup: In order to use TimeGPT model as part of agent forecast workflow, set the TimeGPT credentials. 
  - See [TimeGPT Setup section](https://microsoft.github.io/finnts/articles/forecasting-genai.html#authentication-and-setup) for details.

- Chronos2 Setup: In order to use Chronos2 model as part of agent forecast workflow, set the Chronos2 API credentials.
  - See [Chronos2 Setup section](https://microsoft.github.io/finnts/articles/forecasting-genai.html#chronos2) for details.

- Chronos Bolt Base Setup: Uses the same API credentials as Chronos2. It is a lighter-weight model that does not support external regressors and runs as a local model only.
  - See [Chronos Bolt Base section](https://microsoft.github.io/finnts/articles/forecasting-genai.html#chronos-bolt-base) for details.

- Chronos Bolt Tiny Setup: Uses the same API credentials as Chronos2/Chronos Bolt Base. It is the smallest/fastest model variant that does not support external regressors and runs as a local model only.
  - See [Chronos Bolt Tiny section](https://microsoft.github.io/finnts/articles/forecasting-genai.html#chronos-bolt-tiny) for details.

- TimesFM Setup: Uses its own API endpoint (`TIMESFM_API_URL` and `TIMESFM_API_TOKEN`). It is a local-only model that does not support external regressors.
  - See [TimesFM section](https://microsoft.github.io/finnts/articles/forecasting-genai.html#timesfm) for details.

---

## End-to-end: first run with the AI agent

Below is a complete flow using the built-in M4 monthly sample. 

### 1) Create a project

```{r eval=FALSE}
library(finnts)
library(dplyr)

project <- set_project_info(
  project_name = "ai_agent_demo",
  path = tempdir(), # or a persistent folder
  combo_variables = c("id"),
  target_variable = "value",
  date_type = "month", # day|week|month|quarter|year
  fiscal_year_start = 1 # fiscal month (1 = Jan)
)
```

> Tip: `path` controls where logs/forecasts/EDA artifacts are saved.  
> Supports local filesystem, Azure Blob (via `AzureStor::blob_container`), or Microsoft 365 drives (`ms365r`) via `storage_object`.

### 2) Bring data

- One row per time series combo/date.
- A single Date column named `Date` (class `Date`).
- Target column matches `target_variable`.
- Optional **external regressors** as extra columns (historical **or historical+future** values, if you want them used in forecasting).

```{r eval=FALSE}
hist_data <- timetk::m4_monthly %>%
  dplyr::filter(date >= as.Date("2013-01-01")) %>%
  dplyr::rename(Date = date) %>%
  dplyr::mutate(id = as.character(id))
```

### 3) Define the LLM

`llm` is an ellmer Chat that Finn uses as a configuration template. Finn never mutates this template. It creates an isolated, empty-history session for each forecast series and each `ask_agent()` request, so one modern model can perform both forecast input selection and results analysis without conversation history leaking between workflows.

```{r eval=FALSE}
llm <- ellmer::chat_azure_openai(model = "gpt-4o-mini")
```

### 4) Create the agent run

```{r eval=FALSE}
agent <- set_agent_info(
  project_info = project,
  llm = llm,
  input_data = hist_data,
  forecast_horizon = 6, # number of future periods
  external_regressors = NULL, # e.g., c("Price","Promo")
  allow_hierarchical_forecast = FALSE, # set TRUE to let agent use hierarchies
  negative_forecast = FALSE, # set TRUE to allow forecasts below zero
  overwrite = TRUE # start a fresh run_id if inputs changed
)
```

This writes the versioned inputs into `path/input_data/` (hashed by combo/run) and logs the new `agent_version`/`run_id`.

### 5) Let the agent iterate to a best run

```{r eval=FALSE}
iterate_forecast(
  agent_info          = agent,
  weighted_mape_goal  = 0.05, # your accuracy target of 5%
  max_iter            = 3, # stop after N iterations if not hitting goal
)
```

What happens under the hood:

- Reads cached EDA (or computes it) via the agent's EDA tools.
- Chooses **seasonal period**, **missing/outlier** handling, **box-cox/differencing** strategy.
- Sweeps **models** (local &/or global), backtests, **recipes** (lags/rolling/Fourier/date feats).
- Optionally enables **external regressors** if they improve WMAPE.
- Stops early if MWAPE goal met; otherwise iterates (up to `max_iter`).

### 6) Retrieve results

```{r eval=FALSE}
best_runs <- get_best_agent_run(agent_info = agent, full_run_info = TRUE)
head(best_runs)

fcst <- get_agent_forecast(agent_info = agent)
head(fcst)
```

- `best_runs` summarizes, for each time series combo, the best run inputs when calling the Finn forecast process.
- `fcst` returns the consolidated forecast table (if hierarchical reconciliation was used, this is the reconciled output).

---

## Ask questions about your forecast results

After running `iterate_forecast()` or `update_forecast()`, you can use `ask_agent()` to ask natural language questions about your results. The agent analyzes your forecast data, model configurations, and EDA outputs to provide data-driven answers.

### How it works

`ask_agent()` creates an LLM-driven workflow that:
1. **Plans** the analysis steps needed to answer your question
2. **Executes** R code to analyze the relevant data
3. **Generates** a natural language answer based on the results

### Example questions

```{r eval=FALSE}
# Ask about forecast accuracy
answer <- ask_agent(
  agent_info = agent,
  question = "What is the average weighted MAPE across all time series?"
)

# Ask about models used
answer <- ask_agent(
  agent_info = agent,
  question = "Which models were selected as best for each time series?"
)

# Ask about feature importance
answer <- ask_agent(
  agent_info = agent,
  question = "What are the top 3 most important features for the forecast models?"
)

# Ask about data quality
answer <- ask_agent(
  agent_info = agent,
  question = "Were there any missing values or outliers in the data?"
)

# Ask about specific forecasts
answer <- ask_agent(
  agent_info = agent,
  question = "What are the forecasted values for M750 for the next 3 months?"
)

# Ask about time series characteristics
answer <- ask_agent(
  agent_info = agent,
  question = "Which time series show strong seasonality patterns?"
)

# Ask comparative questions
answer <- ask_agent(
  agent_info = agent,
  question = "Which time series have the highest forecast uncertainty?"
)
```

### What data sources are available

`ask_agent()` has access to four main data sources:

1. **Forecast results** (`get_agent_forecast()`): Future predictions, back-test results, model selections, confidence intervals
2. **Model configurations** (`get_best_agent_run()`): Feature engineering settings, transformations applied, model hyperparameters
3. **EDA results** (`get_eda_data()`): Time series characteristics, seasonality, stationarity tests, data quality metrics
4. **Model summaries** (`get_summarized_models()`): Feature importance, model parameters, recipe details

The agent automatically determines which data sources to use based on your question.

### Tips for effective questions

- Be specific about what you want to know
- Reference specific time series by their combo ID if needed
- Ask about metrics, patterns, or comparisons
- The agent can perform calculations and aggregations on the fly

---

## Updating with new data (production loop)

When you have **new input data**, keep the same project and create a **new agent run** with updated `input_data`. Then call `update_forecast()`:

```{r eval=FALSE}
# suppose you've appended more months to hist_data:
hist_data2 <- hist_data %>% dplyr::filter(Date <= as.Date("2016-06-01"))

agent2 <- set_agent_info(
  project_info = project,
  llm = llm,
  input_data = hist_data2,
  forecast_horizon = 6,
  overwrite = TRUE # required to create a new agent version when running update_forecast()
)

update_forecast(
  agent_info             = agent2,
  weighted_mape_goal     = 0.05,
  allow_iterate_forecast = TRUE, # if degradation detected, allow the agent to re-iterate
  max_iter               = 2 # cap re-iteration cost
)

updated_fcst <- get_agent_forecast(agent2)

# Ask questions about the updated forecast
answer <- ask_agent(
  agent_info = agent2,
  question = "Summarize the forecast accuracy."
)
```

What `update_forecast()` does:

- **Rebuilds global** models first (if used), then updates local series that need it.
- **Handles new time series**: If new series appear in the data (up to 20% of existing series, floor of 10), simple forecasts are created automatically using default local model inputs—no LLM involvement needed.
- **Handles failed time series**: If individual time series fail during the global or local model update (e.g., due to data issues or model errors), they are automatically re-forecast using the same default local model inputs as new series. If more than 20% of existing series (floor of 10) fail to update, the run errors out and directs you to use `iterate_forecast()` to retrain from scratch.
- Compares WMAPE to a trailing baseline of previous runs. If >40% of series are **>20% worse** than the previous run WMAPE, and `allow_iterate_forecast = TRUE`, it will **invoke the iterate loop** (bounded by `max_iter`) to recover accuracy.
- Re-runs reconciliation if hierarchy is in play.

---

## Hierarchies (optional)

Set `allow_hierarchical_forecast = TRUE` in `set_agent_info()` to let the agent detect:

- *none* → use `bottoms_up` (default),
- *standard hierarchy* (e.g., Region → Country → SKU),
- *grouped hierarchy* (crossed dimensions).

When the agent selects a hierarchy, it will:
- train at the selected aggregate(s),
- **reconcile** down to the bottom level,
- produce a **reconciled** `get_agent_forecast()` output.

For background and manual control, see the **"Hierarchical Forecasting"** vignette.

---

## External regressors (xregs)

If you pass `external_regressors = c("Price","Promo", ...)`:

- Provide **historical or historical+future values** (length ≥ horizon) in your `input_data` for the selected columns.
- The agent will **test** which specific xregs help; if they don't, it will disable them for that iteration.
- See the **"External Regressors"** vignette for data prep details.

---

## Parallelism knobs

- `parallel_processing = "local_machine"` runs each time series in parallel across local cores.  
- `parallel_processing = "spark"` executes combos on an Azure Databricks/Synapse Spark cluster (see **"Parallel Processing"** vignette).  
- `inner_parallel = TRUE` parallelizes work **inside** a combo (useful when outer parallelism is `NULL` or `"spark"`).  
- `num_cores = NULL` defaults to *all cores minus one*.

Every time-series combo receives independent driver and reasoning Chat objects with empty conversation history, whether execution is sequential or parallel. Parallel runs require ellmer 0.4.0 or later on the driver and every worker; Finn serializes the configured Chats through `foreach` before creating the per-combo deep clones.

---

## Reading artifacts directly (optional)

You normally won't need this, but for audits:

- Inputs: `path/input_data/…`
- EDA:    `path/eda/…`
- Logs:   `path/logs/…` (includes the hashed `*-agent_run.csv` and `*-agent_best_run.*` for each version)
- Final Agent Outputs: `path/final_output/…`

Use the helpers first; dig into files only if you must.
