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
96 changes: 86 additions & 10 deletions implementations/boc_rate_decisions/99_starter_agent.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,18 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 5,
"id": "cell-01",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"RUN_AGENT = True | model = gemini-3.1-flash-lite-preview\n"
]
}
],
"source": [
"import warnings\n",
"from pathlib import Path\n",
Expand All @@ -48,7 +56,7 @@
"# ── Run guard ──────────────────────────────────────\n",
"# Live agent calls cost tokens and need PROXY_* in the repo-root .env, plus warm\n",
"# data caches. Default False so `Run All` is safe; set True to call the model.\n",
"RUN_AGENT = False\n",
"RUN_AGENT = True\n",
"\n",
"from boc_rate_decisions.starter_agent import (\n",
" build_starter_agent_config,\n",
Expand All @@ -72,10 +80,32 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 6,
"id": "cell-03",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Agent: boc_starter_agent\n",
"Search enabled: True\n",
"Code-exec enabled: False\n",
"Skills loaded: ['forecasting', 'research-playbook']\n",
"\n",
"── System instruction (edit this in starter_agent/agent.py) ──\n",
"\n",
"## Role\n",
"\n",
"You are a Bank of Canada monetary-policy analyst — fluent in the policy-rate path, the 2% CPI inflation target, labour-market and bond-market conditions, and the Bank's institutional behaviour (gradualism, data dependence, reluctance to surprise markets). This is a starter agent: keep your reasoning transparent and your claims honest.\n",
"\n",
"## How to respond\n",
"\n",
"- For open-ended questions, scenario analysis, or anything conversational, answer directly and concisely — do NOT ask for a JSON payload.\n",
"- When you are handed a task that asks for a structured probability distribution over the next decision, produce a calibrated one. ...\n"
]
}
],
"source": [
"config = build_starter_agent_config(\n",
" model=AGENT_MODEL,\n",
Expand Down Expand Up @@ -104,10 +134,40 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 7,
"id": "cell-05",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Following the Bank of Canada’s (BoC) decision on October 23, 2024, to cut the policy rate by 50 basis points to 3.75%, the Governing Council has shifted toward a more balanced, data-dependent approach. The next decision is scheduled for December 11, 2024.\n",
"\n",
"### The Case for a Cut\n",
"* **Preventing Undershooting:** With inflation at 1.6% (September data), it has dipped below the 2% target. A further cut helps pull inflation back toward the midpoint, ensuring it doesn't drift persistently lower in an economy with excess supply.\n",
"* **Supporting Growth:** The economy remains in excess supply, and the labour market is soft, particularly for youth and newcomers. Continued normalization is viewed as necessary to provide support to aggregate demand.\n",
"\n",
"### The Case for a Hold\n",
"* **Assessment of Transmission:** After an \"oversized\" 50bp move, the Bank may prefer to pause to evaluate the impact of recent easing on household spending and business investment before committing to further aggressive moves.\n",
"* **Gradualism:** The Bank typically avoids back-to-back oversized cuts unless economic data signals a rapid deterioration. A 25bp cut or a hold is more consistent with the Bank’s historical preference for predictable, incremental policy adjustments.\n",
"\n",
"### Conclusion: What is more likely?\n",
"**A 25-basis-point cut is the most likely outcome.**\n",
"\n",
"While the 50bp move in October was a strong signal of the Bank’s intent to normalize policy quickly, a 25bp move in December allows the Bank to maintain the momentum of easing while exercising the caution typical of their \"data-dependent\" framework. A hold is less likely unless incoming data (particularly jobs and CPI) shows a surprising and sudden stabilization that negates the need for further stimulus. \n",
"\n",
"**Summary:** The Governing Council's focus has transitioned from fighting high inflation to supporting the economy toward a soft landing; they are firmly in an \"easing cycle,\" making a cut more probable than a hold.\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"Root node boc_starter_agent was cancelled.\n"
]
}
],
"source": [
"from aieng.forecasting.methods.agentic import build_adk_agent\n",
"from aieng.forecasting.methods.agentic.adk_runner import AdkTextRunner, AdkTextRunnerConfig\n",
Expand Down Expand Up @@ -140,10 +200,26 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 8,
"id": "cell-07",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Decision 2026-04-29 forecast from as_of=2026-04-01 (T-28)\n",
"Actual outcome: HOLD\n",
"\n",
" outcome agent prob climatology\n",
" cut 25.00% 10.87%\n",
" hold 70.00% 76.09% <- ACTUAL\n",
" hike 5.00% 13.04%\n",
"\n",
"Agent put 70% on what happened (its top pick ✓).\n"
]
}
],
"source": [
"from datetime import datetime, timezone\n",
"\n",
Expand Down Expand Up @@ -238,7 +314,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
"version": "3.12.12"
}
},
"nbformat": 4,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Also in this README: [Setup](#setup) · [Core concepts](#core-concepts) · [Repo
- **Core library** — `aieng-forecasting` (`aieng.forecasting`): data services, cutoff enforcement, forecasting tasks, prediction payloads, backtesting, evaluation, and artifacts.
- **Reusable methods** — `aieng.forecasting.methods`: `Predictor` implementations including naive baselines (continuous, binary, and categorical), Darts numerical predictors, LLM-process predictors (continuous, binary-probability, and categorical-probability), and ADK-based agentic infrastructure (`build_adk_agent`, `AdkTextRunner`, `AgentPredictor`).
- **Reference implementations** — `implementations/<use-case>/`: notebooks, helper modules, task-specific configuration, and co-located YAML specs.
- **Tracing** — Langfuse / OpenTelemetry bootstrap (`aieng.forecasting.langfuse_tracing`) for LiteLLM and Google ADK.
- **Tracing** — Langfuse / OpenTelemetry bootstrap (`aieng.forecasting.langfuse_tracing`) for LiteLLM and Google ADK. Agent `search_web` calls nest inner search and leakage-verifier generations in the same trace.
- **Data scripts** — `scripts/`: one fetch script per data source, plus `build_e2b_template.py` for the agentic code-execution sandbox.

## Two ways to use a forecaster
Expand Down Expand Up @@ -111,6 +111,8 @@ When you open a **Coder workspace**, startup runs automatically in the backgroun

**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.

**ADK web UI.** [Guide 5](guides/05-access-adk-web-via-ssh-tunnel.md) is how you serve the concierge (or any other bootcamp agent) in the browser. On Coder, `adk web` binds to `localhost` *inside* the workspace — the same guide tunnels that port to your laptop (macOS, Windows, and Linux). Skip the tunnel half if you are running the repo locally.

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:
Expand Down Expand Up @@ -207,7 +209,7 @@ uv run pre-commit run --all-files
## Documentation

- Per-implementation READMEs under [`implementations/`](implementations/) — the primary user surface.
- [`guides/`](guides/) — self-contained, step-by-step strategy guides for the most common build-phase tasks: onboarding a dataset, creating an experiment, customizing an agent's strategy, and auditing a result before you believe it.
- [`guides/`](guides/) — self-contained, step-by-step strategy guides for the most common build-phase tasks: onboarding a dataset, creating an experiment, customizing an agent's strategy, and auditing a result before you believe it. [Guide 5](guides/05-access-adk-web-via-ssh-tunnel.md) is how you serve the concierge or any bootcamp agent under `adk web` (and tunnel that UI from Coder to your laptop).
- [Architecture atlas](https://vectorinstitute.github.io/agentic-forecasting/architecture-atlas.html) ([source](docs/architecture-atlas.html)) — a self-contained visual atlas of the system architecture: the loop, the temporal fence, predictor families, the harness, agent anatomy, and how each reference implementation instantiates them.
- [`aieng-forecasting/README.md`](aieng-forecasting/README.md) and [`aieng-forecasting/aieng/forecasting/methods/README.md`](aieng-forecasting/aieng/forecasting/methods/README.md) — the library and the method catalog.
- [`planning-docs/roadmap.md`](planning-docs/roadmap.md) — architecture principles and extension ideas.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Call :func:`init_langfuse_tracing` once at process startup when using the
``llm`` or ``agentic`` extras and Langfuse credentials are set in the
environment.

Inner LiteLLM calls that ADK OpenInference does not see (``search_web``'s
grounded search and leakage verifier) should wrap with
:func:`langfuse_generation` so they nest under the active agent trace.

Call :func:`print_langfuse_trace_url` after a ``predict()`` call to flush
pending spans and print a clickable Langfuse UI link.
"""
Expand All @@ -17,11 +21,20 @@ from __future__ import annotations

import logging
import os
from contextlib import contextmanager
from typing import Any, Iterator


logger = logging.getLogger(__name__)


class _NoOpObservation:
"""Stand-in when Langfuse is unavailable so callers can always ``.update()``."""

def update(self, **_kwargs: Any) -> None:
return None


def _langfuse_credentials_present() -> bool:
pub = os.environ.get("LANGFUSE_PUBLIC_KEY", "").strip()
sec = os.environ.get("LANGFUSE_SECRET_KEY", "").strip()
Expand Down Expand Up @@ -93,6 +106,48 @@ class _LangfuseTracingBootstrap:
_bootstrap = _LangfuseTracingBootstrap()


@contextmanager
def langfuse_generation(
name: str,
*,
model: str | None = None,
input: Any = None, # noqa: A002 — matches Langfuse observation field name
metadata: dict[str, Any] | None = None,
) -> Iterator[Any]:
"""Open a Langfuse generation nested under the current observation.

Used for inner LiteLLM calls that ADK OpenInference does not see (the
``search_web`` googleSearch completion and the independent leakage
verifier). When an ADK tool span is already active, the new generation
becomes its child, so verifier traces show up inside the agent tree
rather than as a separate root.

No-op when credentials are absent or the SDK raises, so tool code can
wrap completions without a tracing extra.
"""
if not _langfuse_credentials_present():
yield _NoOpObservation()
return
try:
from langfuse import get_client # noqa: PLC0415

kwargs: dict[str, Any] = {"name": name, "as_type": "generation"}
if model is not None:
kwargs["model"] = model
if input is not None:
kwargs["input"] = input
if metadata:
kwargs["metadata"] = metadata
observation_cm = get_client().start_as_current_observation(**kwargs)
except Exception:
logger.debug("langfuse_generation(%s) failed; continuing without a span.", name, exc_info=True)
yield _NoOpObservation()
return

with observation_cm as generation:
yield generation


def init_langfuse_tracing() -> None:
"""Wire LiteLLM and Google ADK to Langfuse.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ from aieng.forecasting.methods.agentic import (
|---|---|---|
| `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` | `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. |
| `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>/`. |
| `agentic/forecast_tool.py` | `ForecastTool` | Conventional ADK `FunctionTool` that runs a pre-specified `Predictor` (AutoARIMA by default) on any registered series at a given cutoff/horizon, returning a structured JSON forecast. A controlled, reproducible alternative to open-ended code execution; series data never enters the LLM context. |
| `agentic/outputs.py` | `AgentForecastOutput` | Abstract output adapter interface for converting structured agent JSON into evaluation `Prediction` objects. |
Expand Down
Loading
Loading