Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d8982f4
First ipman agentic forecasting commit with data
vbeohar Sep 2, 2026
19f86a9
Added agent predictor
vbeohar Sep 22, 2026
3c909b9
Added 5 more macro expl vars
vbeohar Sep 22, 2026
532170e
Merge pull request #1 from VectorInstitute/main
minaattari Sep 22, 2026
255b592
Add XGBoost manufacturing stress predictor
nasas Sep 22, 2026
6722520
Merge pull request #2 from minaattari/add-xgboost-manufacturing-stress
minaattari Sep 22, 2026
9abcd93
feat(manufacturing): add notebook workbench and cached LLMP backtest
minaattari Sep 23, 2026
6877b06
fix(manufacturing): harden agent backtest evaluation
vbeohar Sep 23, 2026
f9e41e8
Merge pull request #3 from minaattari/branch_mina
pratyushpatodia-dev Sep 23, 2026
ea2bd3b
safety-bug-fix
vbeohar Sep 23, 2026
be1834c
Expanded data fetch. Updated downstream components to ingest expanded…
pratyushpatodia-dev Sep 23, 2026
3f13a08
controlled param sweep
vbeohar Sep 23, 2026
ad6c993
Merge pull request #4 from minaattari/day2-data-fetch-pp
vbeohar Sep 23, 2026
9759cb6
Added NY Fed GSCPI data
vbeohar Sep 23, 2026
6c5baa3
Experimented with GSCPI NYFed data
vbeohar Sep 23, 2026
f6af597
Added stress param hyperparams
vbeohar Sep 24, 2026
47df9d3
Merge origin/main into fix/manufacturing-agent-json-output
vbeohar Sep 24, 2026
d2ca24e
Merge pull request #5 from minaattari/fix/manufacturing-agent-json-ou…
pratyushpatodia-dev Sep 24, 2026
2c70f66
Fix manufacturing-stress credit spread feature and drop GSCPI
vbeohar Sep 24, 2026
bce63ed
Add --stress-threshold-pct flag to the parameter sweep
vbeohar Sep 24, 2026
9ced7b1
Merge pull request #6 from minaattari/fix/credit-spread-full-history
minaattari Sep 25, 2026
0dfa932
Improve manufacturing parameter tuning
minaattari Oct 2, 2026
b885a2c
Update FRED adapter and getting-started notebooks
minaattari Oct 2, 2026
c94c527
Merge remote-tracking branch 'bmo-c2/main' into copilot/push-to-bmo-c2
minaattari Oct 2, 2026
0f32996
Merge branch 'more_regulation_on_classic_methods' into HEAD
minaattari Oct 2, 2026
afd0ea8
Document manufacturing stress expansion plan
minaattari Oct 2, 2026
a43b4d9
Add manufacturing stress hybrid workbench
minaattari Oct 2, 2026
475f2c9
Use bounded hybrid adjustments as forecast probabilities
minaattari Oct 2, 2026
2112239
Add manufacturing adaptive evaluation workbenches
minaattari Oct 5, 2026
0492213
Publish manufacturing reports and planning artifacts
minaattari Oct 5, 2026
062259f
Add manufacturing adaptive comparison report
minaattari Oct 5, 2026
775ca7a
Add manufacturing workbench notebooks and reports
minaattari Oct 5, 2026
ea5aaa8
Add learned manufacturing-strategy skill state, SKILL.md and adaptati…
minaattari Oct 6, 2026
8412850
Remove optional backtest from adaptive agent workbench
minaattari Oct 6, 2026
cc4ea72
Merge pull request #7 from minaattari/copilot/push-to-bmo-c2
vbeohar Oct 7, 2026
5bd7ce1
Manufacturing stress: outcome-grounded adaptive learning and rigorous…
vbeohar Oct 7, 2026
aef3a51
Manufacturing walk-forward: separate periodic strategy review step
vbeohar Oct 7, 2026
0e6c584
Add adaptive walk-forward results: lite with and without strategy rev…
vbeohar Oct 7, 2026
ff1e4aa
Final decks and edits
vbeohar Oct 9, 2026
c26147c
Merge pull request #8 from minaattari/manufacturing-enhancements
minaattari Oct 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ implementations/**/data/
!implementations/energy_oil_forecasting/data/predictions/
!implementations/energy_oil_forecasting/data/predictions/**

# Manufacturing workbench reports are reproducible project artifacts.
!implementations/manufacturing_stress_forecasting/reports/
!implementations/manufacturing_stress_forecasting/reports/**

# workspace-specific agents
.github/agents/
.github/prompts/
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Project shape to keep in mind:
- The core library `aieng.forecasting` owns stable infrastructure; reusable predictors live in `aieng.forecasting.methods`; use-case material lives in `implementations/<use-case>/`.
- Strategy guides live under `guides/` (onboard a dataset, create an experiment, customize an agent, audit a result).
- YAML specs are co-located under `implementations/<use-case>/specs/`.
- Reference implementations: Getting Started, Food Price Forecasting, Energy/Oil (stateless capability track plus an adaptive learning agent), BoC Rate Decisions (quantitative path, cutoff-aware press-release ingestion, and a reasoning-alignment evaluator), and S&P 500 (in active development).
- Reference implementations: Getting Started, Food Price Forecasting, Energy/Oil (stateless capability track plus an adaptive learning agent), BoC Rate Decisions (quantitative path, cutoff-aware press-release ingestion, and a reasoning-alignment evaluator), Manufacturing Stress (binary IPMAN stress with numerical baselines, hybrid and adaptive agents, and an outcome-grounded walk-forward), and S&P 500 (in active development).
- Energy/oil's older information-session notebooks are archived under `playground/energy_case_study/`.
- Continuous and discrete-event forecasts are output modalities; numerical methods, LLM Processes, and agentic forecasters are method families that apply to either.

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The repo has two layers. A small core library (`aieng.forecasting`) owns cutoff-
| 2 | [Food price forecasting](implementations/food_price_forecasting/) | Multivariate Canadian food CPI in the style of Canada's Food Price Report (nine sub-indices, 12-month trajectory, avg/avg YoY) | Naive last-value, AutoARIMA; report-grounded LLM-Process (quantile grid and sampled trajectory) |
| 3 | [Energy / oil](implementations/energy_oil_forecasting/) | Daily WTI crude under regime-breaking news (continuous trajectory, binary up-shock, scenario analysis) | Prophet, LLM-Process, news-grounded agent, code-executing agent, adaptive (curriculum-trained) agent |
| 4 | [BoC rate decisions](implementations/boc_rate_decisions/) | Will the Bank of Canada cut, hold, or hike at its next meeting? (ordered categorical; binary cut-vs-not special case) | Climatological frequency, multinomial logistic, categorical LLM-Process, analyst agent; LLM-as-judge reasoning alignment |
| 5 | [Manufacturing stress](implementations/manufacturing_stress_forecasting/) | Will U.S. manufacturing (IPMAN) be under stress three months ahead? (binary, monthly origins) | Historical frequency, logistic, XGBoost; analyst, anchor-bounded hybrid, and adaptive agents, including a walk-forward agent that learns from published outcomes; block-bootstrap and Diebold-Mariano comparisons |

Also in this README: [Setup](#setup) Β· [Core concepts](#core-concepts) Β· [Repository layout](#repository-layout) Β· [Documentation](#documentation)

Expand Down Expand Up @@ -138,7 +139,7 @@ New to the project? Open [`implementations/getting_started/00_environment_check.

### Populate the data cache

Data is fetched once and cached locally (gitignored). Each implementation names the fetch script(s) it needs in its own `README.md` β€” for example `scripts/fetch_cpi.py` (getting started), `scripts/fetch_sp500_market.py` + `scripts/fetch_fred.py` (S&P 500), `scripts/fetch_wti.py` (energy), and `scripts/fetch_boc.py` and `scripts/fetch_boc_press_releases.py` (BoC). Run the relevant one before opening that implementation's notebooks:
Data is fetched once and cached locally (gitignored). Each implementation names the fetch script(s) it needs in its own `README.md` β€” for example `scripts/fetch_cpi.py` (getting started), `scripts/fetch_sp500_market.py` + `scripts/fetch_fred.py` (S&P 500), `scripts/fetch_wti.py` (energy), `scripts/fetch_boc.py` and `scripts/fetch_boc_press_releases.py` (BoC), and `scripts/fetch_manufacturing_stress.py` (manufacturing stress). Run the relevant one before opening that implementation's notebooks:

```bash
uv run python scripts/fetch_cpi.py
Expand Down
3 changes: 2 additions & 1 deletion aieng-forecasting/aieng/forecasting/data/adapters/fred.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ def __init__(
refresh: bool = False,
) -> None:
self._series_id = series_id
self._api_key = api_key or os.environ.get("FRED_API_KEY")
raw_key = (api_key if api_key is not None else os.environ.get("FRED_API_KEY", "")).strip()
self._api_key = raw_key or None
self._cache_dir = Path(cache_dir) if cache_dir is not None else None
self._refresh = refresh

Expand Down
4 changes: 2 additions & 2 deletions aieng-forecasting/aieng/forecasting/methods/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ from aieng.forecasting.methods.agentic import (

| Module | Class / Function | Description |
|---|---|---|
| `agentic/adk_runner.py` | `AdkTextRunner` | Async text-in / text-out wrapper around ADK `InMemoryRunner`. Manages ADK sessions (fresh-per-message or sticky) and optionally traces each turn to Langfuse via `propagate_attributes`. |
| `agentic/adk_runner.py` | `AdkTextRunner` | Async text-in / text-out wrapper around ADK `InMemoryRunner`. Drains each event stream, joins non-thought text parts from the first usable final response, prefers structured JSON submitted through `set_model_response`, manages ADK sessions (fresh-per-message or sticky), and optionally traces each turn to Langfuse via `propagate_attributes`. |
| `agentic/adk_runner.py` | `AdkTextRunnerConfig` | Pydantic configuration for `AdkTextRunner` (session mode, Langfuse fields). |
| `agentic/agent_factory.py` | `build_adk_agent` | Generic ADK `LlmAgent` factory with optional code execution, context retrieval, skills, generation controls, and structured output schema. `search_web` runs a cutoff-aware googleSearch sub-call plus an independent leakage verifier on historical origins (skipped when `as_of` is today or later); both inner LLM calls emit nested Langfuse generations (`search_web.google_search`, `search_web.leakage_verifier`) under the agent trace. |
| `agentic/agent_factory.py` | `AgentConfig` | Pydantic configuration for reusable ADK agents. `output_schema=None` supports interactive/free-form agents; a structured `AgentForecastOutput` schema supports Track 1 predictors. The `function_tools` field attaches conventional ADK tools (e.g. `ForecastTool`). Use-case-specific prompts and presets should live in `implementations/<use-case>/`. |
Expand All @@ -111,5 +111,5 @@ from aieng.forecasting.methods.agentic import (
| `agentic/outputs.py` | `ContinuousAgentForecastOutput` | Canonical continuous forecasting output schema. Declares `modality = "continuous"`, requires one forecast per task horizon and the standard quantile grid, then converts to `ContinuousForecast` payloads. |
| `agentic/outputs.py` | `DiscreteAgentForecastOutput` | Binary event output schema (`modality = "discrete"`): one probability plus `reasoning` / `key_signals` metadata, converted to a `BinaryForecast` payload. |
| `agentic/outputs.py` | `CategoricalAgentForecastOutput` | Ordered-categorical output schema (`modality = "categorical"`): one `{label, probability}` row per task category, validated against `task.categories` and converted to a `CategoricalForecast` payload. |
| `agentic/predictor.py` | `AgentPredictor` | Track 1 `Predictor` that builds prompts, runs an ADK agent through `AdkTextRunner`, validates structured JSON, and converts it to `Prediction` objects. Accepts an optional injected runner for tests or custom observability. |
| `agentic/predictor.py` | `AgentPredictor` | Track 1 `Predictor` that builds prompts, runs an ADK agent through `AdkTextRunner`, validates structured JSON, and converts it to `Prediction` objects. Accepts an optional injected runner for tests or custom observability; schema-validation failure logs include the Langfuse trace ID when available. |
| `agentic/predictor.py` | `ForecastPromptBuilder` | Protocol for task-specific prompt builders that turn `(task, context)` into the text passed to the agent. |
15 changes: 12 additions & 3 deletions aieng-forecasting/aieng/forecasting/methods/agentic/adk_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,15 +295,24 @@ async def run_text_async(
content = genai_types.Content(role="user", parts=[genai_types.Part(text=prompt)])

async def drain_run() -> str:
final_text: str | None = None
async for event in self._runner.run_async(
user_id=user_id,
session_id=session_id,
new_message=content,
run_config=run_config,
):
if event.is_final_response() and event.content and event.content.parts:
return event.content.parts[0].text or ""
return ""
if final_text is None and event.is_final_response() and event.content and event.content.parts:
text_parts = [
part.text
for part in event.content.parts
if isinstance(part.text, str)
and part.text.strip()
and getattr(part, "thought", False) is not True
]
if text_parts:
final_text = "".join(text_parts)
return final_text or ""

async def run_and_resolve() -> str:
"""Run the agent and return the best available output string.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,11 @@ def predict(self, task: ForecastingTask, context: ForecastContext) -> list[Predi
try:
output = self.output_schema.model_validate(json.loads(output_str))
except Exception:
logger.warning("Raw agent response (schema validation failed):\n%s", output_str)
logger.warning(
"Raw agent response (schema validation failed; langfuse_trace_id=%s):\n%s",
self._runner.last_trace_id,
output_str,
)
raise

# Convert output to list of predictions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,12 @@ def test_missing_api_key_without_cache_raises(tmp_path: Path) -> None:
adapter = FREDAdapter("EXCAUS", api_key=None, cache_dir=cache_dir)
with pytest.raises(ValueError, match="FRED API key not provided"):
adapter.fetch()


def test_api_key_whitespace_is_trimmed() -> None:
"""Whitespace around a pasted key must not be included in the request URL."""
fake = _fred_cls_returning(_raw_fred_series())
with patch("fredapi.Fred", fake):
FREDAdapter("EXCAUS", api_key=" fake-key\n", cache_dir=None, refresh=True).fetch()

assert fake.call_args.kwargs["api_key"] == "fake-key"
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def _final_event(text: str) -> MagicMock:
event.is_final_response.return_value = True
part = MagicMock()
part.text = text
part.thought = False
event.content.parts = [part]
return event

Expand Down Expand Up @@ -373,6 +374,41 @@ async def test_returns_text_from_first_final_event(self, patch_runner_cls, mock_
runner = AdkTextRunner(mock_agent, config=AdkTextRunnerConfig(app_name="app"))
assert await runner.run_text_async("hi") == "hello world"

async def test_joins_final_text_parts_and_ignores_thought_parts(self, patch_runner_cls, mock_agent) -> None:
"""Use final answer text, not internal thought or function-call parts."""
event = _final_event("")
thought = MagicMock()
thought.text = "internal thought"
thought.thought = True
json_start = MagicMock()
json_start.text = '{"probability":'
json_start.thought = False
json_end = MagicMock()
json_end.text = "0.4}"
json_end.thought = False
event.content.parts = [thought, json_start, json_end]
patch_runner_cls.run_async.return_value = _stream(event)

runner = AdkTextRunner(mock_agent, config=AdkTextRunnerConfig(app_name="app"))

assert await runner.run_text_async("forecast") == '{"probability":0.4}'

async def test_drains_stream_after_first_final_event(self, patch_runner_cls, mock_agent) -> None:
"""Consume cleanup events after capturing the final response."""
consumed = False

async def stream_with_trailing_event():
nonlocal consumed
yield _final_event("hello world")
yield _intermediate_event()
consumed = True

patch_runner_cls.run_async.return_value = stream_with_trailing_event()
runner = AdkTextRunner(mock_agent, config=AdkTextRunnerConfig(app_name="app"))

assert await runner.run_text_async("hi") == "hello world"
assert consumed

async def test_returns_empty_string_when_stream_has_no_final_event(self, patch_runner_cls, mock_agent) -> None:
"""Stream with only non-final events yields an empty string."""
patch_runner_cls.run_async.return_value = _stream(_intermediate_event())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,16 @@ def test_schema_validation_errors_are_not_swallowed(self) -> None:
with pytest.raises(ValidationError):
predictor.predict(_task([1]), _context())

def test_schema_error_log_includes_langfuse_trace_id(self, caplog: pytest.LogCaptureFixture) -> None:
"""Include trace IDs in parse failures for easier generation inspection."""
predictor, _ = _make_predictor(response="")
predictor._runner.last_trace_id = "trace-123"

with caplog.at_level(logging.WARNING), pytest.raises(json.JSONDecodeError):
predictor.predict(_task([1]), _context())

assert "langfuse_trace_id=trace-123" in caplog.text


# ---------------------------------------------------------------------------
# Async/sync bridge
Expand Down
Binary file added data/fred/CPIAUCSL.parquet
Binary file not shown.
Binary file added data/fred/DFF.parquet
Binary file not shown.
Binary file added data/fred/DGS10.parquet
Binary file not shown.
Binary file added data/fred/DGS2.parquet
Binary file not shown.
Binary file added data/fred/FEDFUNDS.parquet
Binary file not shown.
Binary file added data/fred/ICSA.parquet
Binary file not shown.
Binary file added data/fred/IPMAN.parquet
Binary file not shown.
Binary file added data/fred/UNRATE.parquet
Binary file not shown.
Binary file added data/fred/VIXCLS.parquet
Binary file not shown.
Binary file added data/yfinance/spy_adj_close_1d.parquet
Binary file not shown.
Binary file added data/yfinance/xli_adj_close_1d.parquet
Binary file not shown.
4 changes: 3 additions & 1 deletion implementations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ implementations/
| `-- specs/ # backtest and eval YAML
|-- boc_rate_decisions/ # 4 Β· Discrete-event reference: BoC cut/hold/hike direction
| `-- specs/ # direction + binary backtest / eval / smoke YAML
|-- manufacturing_stress_forecasting/ # 5 Β· Binary IPMAN stress, 3 months ahead; numerical, hybrid, and adaptive agents
| `-- specs/ # smoke, full-history, and protected-eval YAML
|-- tests/ # tests for implementation-specific helper modules
`-- pyproject.toml # local workspace packaging
```

YAML backtest and eval specs live under each use case in `specs/`. Each directory is independent; see its `README.md` for the walkthrough. For the build-phase moves β€” onboarding data, standing up an experiment, customizing an agent, auditing a result β€” see [`guides/`](../guides/). To chat with the concierge or a domain starter in the ADK browser UI, see [`guides/05-access-adk-web-via-ssh-tunnel.md`](../guides/05-access-adk-web-via-ssh-tunnel.md) (includes the Coder SSH tunnel).

Every domain use case (all except `getting_started`) also ships a `starter_agent/` module and a `99_starter_agent.ipynb` β€” a fresh, hackable **starter agent** that is the consistent "build your own" entry point for that use case (toggleable news search + code execution, two lightweight tool-usage skills, an interactive cell, and one scored forecast).
Every domain use case except `getting_started` and `manufacturing_stress_forecasting` also ships a `starter_agent/` module and a `99_starter_agent.ipynb` β€” a fresh, hackable **starter agent** that is the consistent "build your own" entry point for that use case (toggleable news search + code execution, two lightweight tool-usage skills, an interactive cell, and one scored forecast).

`getting_started/` additionally ships a **`concierge_agent/`** module and **`99_repo_concierge.ipynb`** β€” a repo onboarding helper (not a forecaster) that answers questions about how the codebase works using a committed public-`main` knowledge digest. From the repository root: `uv run adk run implementations/getting_started/concierge_agent` (or `uv run adk web implementations/getting_started/concierge_agent` for the browser UI β€” [guide 5](../guides/05-access-adk-web-via-ssh-tunnel.md)). See [`getting_started/README.md`](getting_started/README.md) and the notebook for full usage.

Expand Down
Loading