Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# In the Coder environment bootcamp keys (OPENAI_*, E2B_*, LANGFUSE_*) are injected
# into your shell environment β€” do NOT copy those into .env on Coder workspaces.
# Optional personal keys (e.g. FRED_API_KEY) still need to be set in .env.

# E2B Code Execution Service
E2B_API_KEY=your_e2b_api_key

# Vector LLM Proxy Service
PROXY_BASE_URL=https://proxy.vectorinstitute.ai/v1
PROXY_API_KEY=your_proxy_api_key
OPENAI_BASE_URL=https://proxy.vectorinstitute.ai/v1
OPENAI_API_KEY=your_api_key

# Langfuse β€” required for trace logging in playground/news_search and misalignment_qa
LANGFUSE_PUBLIC_KEY=pk-lf-...
Expand All @@ -12,11 +16,3 @@ LANGFUSE_HOST=https://us.cloud.langfuse.com

# Optional keys
FRED_API_KEY=your_fred_api_key # You need to request this from FRED if you want to use it

# Claude Code setup β€” route Claude Code through the Vector proxy.
# Set ANTHROPIC_AUTH_TOKEN to your Vector proxy API key (same key as PROXY_API_KEY).
ANTHROPIC_BASE_URL="https://proxy.vectorinstitute.ai"
ANTHROPIC_AUTH_TOKEN="your_vector_proxy_api_key"
ANTHROPIC_MODEL="Qwen3-Coder-Next"
ANTHROPIC_CUSTOM_MODEL_OPTION="Qwen3-Coder-Next"
ANTHROPIC_CUSTOM_MODEL_OPTION_NAME="Qwen3-Coder-Next"
45 changes: 41 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,13 @@ Each is independent and self-contained β€” pick the one that matches the problem

**Start here β†’ #0 [`getting_started/`](implementations/getting_started/)** β€” one CPI series, one month ahead. The smallest end-to-end loop: a `Predictor`, a `BacktestSpec` and `EvalSpec`, naive + AutoARIMA baselines, CRPS scoring. The place to learn the evaluation framework before picking a domain below.


| # | Implementation | The problem | Concepts & techniques it demonstrates |
| --- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | [`sp500_forecasting/`](implementations/sp500_forecasting/) | S&P 500 returns under a macro/market covariate panel. | A head-to-head of conventional numerical methods (naive, ETS, Kalman, AutoARIMA, linear regression, LightGBM) plus a covariate-aware LLM-Process, all reading the same leak-safe covariate panel. Cumulative-return targets at 1/5/21-business-day horizons, CRPS + direction metrics, config-driven specs. |
| 2 | [`food_price_forecasting/`](implementations/food_price_forecasting/) | A multivariate food-CPI trajectory, in the style of Canada's Food Price Report. | Nine correlated sub-indices, a 12-step trajectory, a domain metric (avg/avg YoY), baselines vs LLM-Process predictors, leakage-aware backtests, and cached artifacts for fast iteration. |
| 3 | [`energy_oil_forecasting/`](implementations/energy_oil_forecasting/) | Daily WTI crude-oil price under regime-breaking news. | A capability progression β€” Prophet β†’ LLM-Process β†’ news-grounded agent β†’ code-executing agent β€” plus an adaptive agent that learns a strategy from data and is scored before vs after. Continuous trajectories, a binary up-shock task, and interactive scenario analysis. |
| 4 | [`boc_rate_decisions/`](implementations/boc_rate_decisions/) | Will the Bank of Canada cut, hold, or hike at its next meeting? | Discrete-event forecasting: ordered-categorical outcomes on an irregular calendar, RPS scoring and one-vs-rest calibration (instead of CRPS), a binary (Brier) special case, cutoff-aware document ingestion, and an LLM-as-judge that scores an agent's reasoning against the official rationale. |


**Not sure where to start building?** Each of the four domain implementations above ends with a `99_starter_agent.ipynb` β€” a fresh, hackable **starter agent** (a `starter_agent/` module) with toggleable news search and code execution, two lightweight tool-usage skills, an interactive cell, and one scored forecast. It's the consistent "continue from here" entry point for taking any reference use case in an agentic direction, and a quick end-to-end test of that use case's agent stack.

## Time Series Data sources
Expand All @@ -62,12 +60,15 @@ Once you have the key, add it to your repo-root `.env`:
FRED_API_KEY=your_fred_api_key
```

On Coder workspaces, bootcamp keys (`OPENAI_*`, `E2B_*`, `LANGFUSE_*`) live in your shell environment β€” **not** in repo `.env`. See [Bootcamp environment](#bootcamp-environment-coder).

## Repository layout

```text
aieng-forecasting/ # Installable library: import as aieng.forecasting
implementations/ # Self-contained reference implementations + co-located specs
scripts/ # Data-fetch scripts + E2B template builder
tests/ # Onboarding integration tests (not run in CI)
planning-docs/ # Architecture notes and the extension/roadmap catalog
playground/ # Exploration and archived demos (not reference implementations)
```
Expand All @@ -79,7 +80,7 @@ Install dependencies from the repo root:
```bash
git clone <repo-url>. # If running locally. Coder environment setup clones repo automatically.
cd agentic-forecasting
uv sync
uv sync --dev
```

**macOS β€” LightGBM and OpenMP.** The library depends on **LightGBM** (used by `DartsLightGBMPredictor` and some notebooks). The PyPI wheel expects **OpenMP** at runtime. If you see `Library not loaded: @rpath/libomp.dylib` when importing or training, install Homebrew's OpenMP once and restart your shell or Jupyter kernel:
Expand All @@ -90,6 +91,39 @@ brew install libomp

On Apple Silicon the dylib is typically under `/opt/homebrew/opt/libomp/lib/`; on Intel Homebrew, `/usr/local/opt/libomp/lib/`.

### Coder Workspaces

When you open a **Coder workspace**, startup runs automatically in the background. By the time you connect you should have:

- The repo cloned, a Python venv, and dependencies installed
- Bootcamp API keys (`OPENAI_*`, `E2B_*`, `LANGFUSE_*`) available in your shell (not in `.env`)
- A shell that opens in the repo with the venv activated

**Your next step:** run [`00_environment_check.ipynb`](implementations/getting_started/00_environment_check.ipynb) top to bottom. That notebook will confirm that startup succeeded.

On first boot, keys are verified against live services and your onboarding status is recorded. Workspace restarts reload keys without re-running the full test suite.

**Local machine or troubleshooting** β€” fetch and verify keys manually:

```bash
eval "$(onboard --bootcamp-name agentic-forecasting --test-script tests/test_integration.py)"
```

Reload keys in a new shell without re-testing:

```bash
eval "$(onboard --bootcamp-name agentic-forecasting --skip-test)"
```

Headless verification (same checks as first-boot onboarding):

```bash
uv sync --all-extras --dev --all-packages
uv run pytest tests/test_integration.py -v
```

**Credential model:** bootcamp keys live in your shell environment. Optional personal keys (e.g. `FRED_API_KEY`) go in a `.env` only β€” see [`.env.example`](.env.example).

### Verify your environment first

New to the project? Open [`implementations/getting_started/00_environment_check.ipynb`](implementations/getting_started/00_environment_check.ipynb) and run it top to bottom. It's a self-guided preflight that checks every major capability β€” proxy LLM inference, Langfuse, E2B code execution, StatCan/FRED data access, and a full end-to-end mini backtest β€” one cell at a time, and tells you exactly what to fix when something isn't set up (most often a missing or placeholder key in your `.env`). It's the fastest way to confirm setup before working through the reference implementations.
Expand All @@ -110,10 +144,13 @@ If this was unsuccessful, or if you prefer to run with E2B in an alternative env

1. Create a free account at [e2b.dev](https://e2b.dev) and copy your API key.
2. Add it to your `.env` file alongside the other keys (see `.env.example`):

```
E2B_API_KEY=your_e2b_api_key
```
3. Build the template (takes a few minutes on first run):

1. Build the template (takes a few minutes on first run):

```bash
uv run --env-file .env scripts/build_e2b_template.py
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ def _build_automatic_function_calling_config(
def _build_search_tool(
config: ContextRetrievalConfig,
*,
proxy_base_url: str,
proxy_api_key: str | None,
openai_base_url: str,
openai_api_key: str | None,
) -> Callable[..., Any]:
"""Return an async ``search_web`` FunctionTool backed by the proxy's googleSearch.

Expand Down Expand Up @@ -243,8 +243,8 @@ async def search_web(query: str, cutoff_date: str | None = None) -> str:
search_model = f"openai/{search_model}"
resp = await litellm.acompletion(
model=search_model,
api_base=proxy_base_url,
api_key=proxy_api_key,
api_base=openai_base_url,
api_key=openai_api_key,
messages=[
{"role": "system", "content": config.instruction},
{"role": "user", "content": user_content},
Expand Down Expand Up @@ -277,16 +277,16 @@ class AgentConfig(BaseModel):
model : str | BaseLlm, default=LITE_MODEL (``"gemini-3.1-flash-lite-preview"``)
Model name (bare, no provider prefix) or a custom
:class:`~google.adk.models.base_llm.BaseLlm` instance. When
``proxy_base_url`` is set and ``model`` is a plain string,
``openai_base_url`` is set and ``model`` is a plain string,
:func:`build_adk_agent` wraps it in a
:class:`~google.adk.models.lite_llm.LiteLlm` instance pointing to
the proxy. Pass a ``BaseLlm`` directly to skip automatic wrapping.
proxy_base_url : str | None, default=PROXY_BASE_URL env var
openai_base_url : str | None, default=OPENAI_BASE_URL env var
Base URL for the OpenAI-compatible LLM proxy. Defaults to the
``PROXY_BASE_URL`` environment variable. When set, the agent (and
``OPENAI_BASE_URL`` environment variable. When set, the agent (and
the ``search_web`` tool) route all calls through the proxy.
proxy_api_key : str | None, default=PROXY_API_KEY env var
API key for the proxy. Defaults to the ``PROXY_API_KEY``
openai_api_key : str | None, default=OPENAI_API_KEY env var
API key for the proxy. Defaults to the ``OPENAI_API_KEY``
environment variable.
description : str, default=""
Description of the agent. Useful when the agent is used as a sub-agent.
Expand Down Expand Up @@ -342,15 +342,15 @@ class AgentConfig(BaseModel):

name: str = "adk_forecasting_agent"
model: str | BaseLlm = LITE_MODEL
proxy_base_url: str | None = Field(
default_factory=lambda: os.getenv("PROXY_BASE_URL"),
openai_base_url: str | None = Field(
default_factory=lambda: os.getenv("OPENAI_BASE_URL"),
description=(
"Base URL for the OpenAI-compatible LLM proxy. Defaults to the PROXY_BASE_URL environment variable."
"Base URL for the OpenAI-compatible LLM proxy. Defaults to the OPENAI_BASE_URL environment variable."
),
)
proxy_api_key: str | None = Field(
default_factory=lambda: os.getenv("PROXY_API_KEY"),
description="API key for the proxy. Defaults to the PROXY_API_KEY environment variable.",
openai_api_key: str | None = Field(
default_factory=lambda: os.getenv("OPENAI_API_KEY"),
description="API key for the proxy. Defaults to the OPENAI_API_KEY environment variable.",
)
description: str = ""
instruction: str = ""
Expand Down Expand Up @@ -404,7 +404,7 @@ def build_adk_agent(
Code execution (E2B) and the web-search context-retrieval tool are wired
only when the corresponding capability blocks in ``config`` are enabled.

When ``config.proxy_base_url`` is set and ``config.model`` is a plain
When ``config.openai_base_url`` is set and ``config.model`` is a plain
string, the model is automatically wrapped in a
:class:`~google.adk.models.lite_llm.LiteLlm` instance that routes all
calls through the proxy. Pass a ``BaseLlm`` instance directly to bypass
Expand Down Expand Up @@ -452,7 +452,7 @@ def build_adk_agent(
"""
# Resolve model: wrap bare string in LiteLlm when proxy is configured.
model: str | BaseLlm = config.model
if isinstance(model, str) and config.proxy_base_url:
if isinstance(model, str) and config.openai_base_url:
from google.adk.models.lite_llm import LiteLlm # noqa: PLC0415

# Prefix with "openai/" so LiteLLM uses the OpenAI-compatible path.
Expand All @@ -461,8 +461,8 @@ def build_adk_agent(
litellm_model = model if model.startswith("openai/") else f"openai/{model}"
model = LiteLlm(
model=litellm_model,
api_base=config.proxy_base_url,
api_key=config.proxy_api_key,
api_base=config.openai_base_url,
api_key=config.openai_api_key,
)

# Configure tools
Expand All @@ -478,12 +478,12 @@ def build_adk_agent(
)

if config.context_retrieval.enabled:
proxy_base_url = config.proxy_base_url or os.getenv("PROXY_BASE_URL") or ""
openai_base_url = config.openai_base_url or os.getenv("OPENAI_BASE_URL") or ""
tools.append(
_build_search_tool(
config.context_retrieval,
proxy_base_url=proxy_base_url,
proxy_api_key=config.proxy_api_key,
openai_base_url=openai_base_url,
openai_api_key=config.openai_api_key,
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,22 @@ class LLMPredictorConfig(BaseModel):
description=(
"Model name as expected by the proxy (bare, no provider prefix), "
"e.g. 'gemini-3.1-flash-lite-preview', 'gpt-4o-mini'. "
"When proxy_base_url is set, LiteLLM routes this to the proxy via "
"When openai_base_url is set, LiteLLM routes this to the proxy via "
"custom_llm_provider='openai'."
),
)
proxy_base_url: str | None = Field(
default_factory=lambda: os.getenv("PROXY_BASE_URL"),
openai_base_url: str | None = Field(
default_factory=lambda: os.getenv("OPENAI_BASE_URL"),
description=(
"Base URL for an OpenAI-compatible LLM proxy. Defaults to the "
"``PROXY_BASE_URL`` environment variable. When set, all completions "
"``OPENAI_BASE_URL`` environment variable. When set, all completions "
"are routed through the proxy using ``api_base`` + "
"``custom_llm_provider='openai'``."
),
)
proxy_api_key: str | None = Field(
default_factory=lambda: os.getenv("PROXY_API_KEY"),
description=("API key for the proxy. Defaults to the ``PROXY_API_KEY`` environment variable."),
openai_api_key: str | None = Field(
default_factory=lambda: os.getenv("OPENAI_API_KEY"),
description=("API key for the proxy. Defaults to the ``OPENAI_API_KEY`` environment variable."),
)
temperature: float = Field(default=1.0, ge=0.0, le=2.0, description="Sampling temperature.")
max_tokens: int = Field(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ def _sample_probability(
max_tokens=cfg.max_tokens,
timeout_s=cfg.timeout_s,
reasoning_effort=cfg.reasoning_effort,
api_base=cfg.proxy_base_url,
api_key=cfg.proxy_api_key,
api_base=cfg.openai_base_url,
api_key=cfg.openai_api_key,
),
)
if not parsed:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,8 @@ def _sample_distribution(
max_tokens=cfg.max_tokens,
timeout_s=cfg.timeout_s,
reasoning_effort=cfg.reasoning_effort,
api_base=cfg.proxy_base_url,
api_key=cfg.proxy_api_key,
api_base=cfg.openai_base_url,
api_key=cfg.openai_api_key,
),
)
if not parsed:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,8 @@ def _sample_quantile_grid(
max_tokens=cfg.max_tokens,
timeout_s=cfg.timeout_s,
reasoning_effort=cfg.reasoning_effort,
api_base=cfg.proxy_base_url,
api_key=cfg.proxy_api_key,
api_base=cfg.openai_base_url,
api_key=cfg.openai_api_key,
),
)
if not parsed:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ def _sample_trajectories(
max_tokens=cfg.max_tokens,
timeout_s=cfg.timeout_s,
reasoning_effort=cfg.reasoning_effort,
api_base=cfg.proxy_base_url,
api_key=cfg.proxy_api_key,
api_base=cfg.openai_base_url,
api_key=cfg.openai_api_key,
),
)
return result
Expand Down
2 changes: 1 addition & 1 deletion aieng-forecasting/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ documents = [
"pyyaml>=6.0",
]
agentic = [
"aieng-agents[code-interpreter]>=0.3.0",
"aieng-agents[code-interpreter]>=0.3.1",
"google-adk>=2.2.0",
"google-cloud-storage>=2.18,<4",
"langfuse>=4.5.1",
Expand Down
Loading
Loading