From 59ec21cc9d5179a194ebd316ae9dd3709c07ae22 Mon Sep 17 00:00:00 2001 From: Owen Price Skelly <21372141+OwenPriceSkelly@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:35:10 -0600 Subject: [PATCH 1/4] add academy example --- examples/academy/README.md | 73 ++++++++++++++++++++++++++++++++ examples/academy/agent.py | 83 +++++++++++++++++++++++++++++++++++++ examples/academy/compute.py | 39 +++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 examples/academy/README.md create mode 100644 examples/academy/agent.py create mode 100644 examples/academy/compute.py diff --git a/examples/academy/README.md b/examples/academy/README.md new file mode 100644 index 0000000..7189cba --- /dev/null +++ b/examples/academy/README.md @@ -0,0 +1,73 @@ +# Academy + Groundhog Integration Example + +This example demonstrates how [Academy](https://github.com/academy-agents/academy) agents can leverage Groundhog HPC for isolated compute with automatic dependency management. + +## Value Proposition + +**Problem:** Academy agents need to perform computations requiring specialized packages (numpy, scipy, pytorch, etc.), but installing all dependencies in the agent environment is brittle and prone to version conflicts. + +**Solution:** Groundhog provides isolated execution with automatic dependency management via PEP 723 inline script metadata. Functions decorated with `@hog.function()` can be called with: +- `.local()` - Subprocess isolation on your local machine +- `.remote()` - Execution on HPC clusters via Globus Compute + +The agent environment stays clean (only `academy-py` + `groundhog-hpc`), while the groundhog function brings in dependencies on-demand. + +## Files + +- **`compute.py`** - Groundhog script exposing `predict_season` function that uses numpy +- **`agent.py`** - Academy agent that calls the function with and without isolation + +## Running the Example + +```bash +uv run --with academy-py --with groundhog-hpc agent.py +``` + +### Expected Output + +The agent will: +1. Attempt direct function call → **fails** (numpy not in main agent environment) +2. Use `predict_season.local()` → **succeeds** (Groundhog subprocess has numpy) +3. Display season prediction based on shadow measurements + +Example output: +``` +Shadow measurements: [3.9, 2.9, 4.0, 1.1, 2.4, 2.6, 1.3, 2.4, 3.8, 1.2] + +Attempting direct call (will fail - numpy not in environment)... +Direct call failed (as expected): No module named 'numpy' + +Using Groundhog .local() (succeeds - isolated subprocess)... +[local] Installed 1 package in 33ms +.local() call succeeded! + + Prediction: MORE WINTER🌤️🦫 + Shadow index: 2.96 + Confidence: extremely high + +``` + + +### Integration + +Academy agents can call Groundhog functions (i.e. decorated functions imported from a groundhog script) just like any Python function: + +```python +from compute import predict_season +... + +class GroundhogAgent(Agent): + @action + async def predict(self, shadow_measurements: list[float]) -> dict: + # Direct call would fail (no numpy) + # result = predict_season(shadow_measurements) + + # Isolated call succeeds + return predict_season.local(shadow_measurements) +``` + +## See Also + +- [Academy Documentation](https://docs.academy-agents.org/) +- [Groundhog Documentation](https://groundhog-hpc.readthedocs.io/en/latest/) +- [Globus Compute](https://docs.globus.org/compute) diff --git a/examples/academy/agent.py b/examples/academy/agent.py new file mode 100644 index 0000000..f59810a --- /dev/null +++ b/examples/academy/agent.py @@ -0,0 +1,83 @@ +"""Academy agent that uses Groundhog for isolated compute with dependencies. + +This example demonstrates how Academy agents can leverage Groundhog to execute +functions requiring packages not in the agent's environment (numpy in this case). + +Key benefits: +- Agent environment stays clean (only academy-py + groundhog-hpc needed) +- Compute dependencies isolated in subprocess via .local() +- Same code can dispatch to Globus Compute endpoints via .remote() + +Run: uv run --with academy-py --with groundhog-hpc agent.py +""" + +import asyncio +import random + +from academy.agent import Agent, action +from academy.exchange import LocalExchangeFactory +from academy.manager import Manager +from concurrent.futures import ThreadPoolExecutor + +import groundhog_hpc # noqa: F401 + +from compute import predict_season + + +class GroundhogAgent(Agent): + """Agent that uses an environment-sensitive scientific instrument (🌤️🦫🕳️) to predict the weather.""" + + @action + async def predict(self, shadow_measurements: list[float]) -> dict[str, str | float]: + """Make a groundhog-backed inference. + + Demonstrates groundhog's dependency isolation in an agentic context: + - Direct call fails (numpy not in agent environment) + - .local() succeeds (Groundhog manages isolated subprocess with numpy) + - Could use .remote() to dispatch to HPC clusters via Globus Compute + """ + # First, try calling directly - this will fail since numpy isn't available + print("Attempting direct call (will fail - numpy not in environment)...") + try: + result = predict_season(shadow_measurements) + except ImportError as e: + print(f"Direct call failed (as expected): {e}") + + # Now use Groundhog's .local() for isolated execution with dependencies + print("\nUsing Groundhog .local() (succeeds - isolated subprocess)...") + result = predict_season.local(shadow_measurements) + print(f".local() call succeeded!") + + # Or, dispatch to a Globus Compute endpoint + # result = predict_season.remote( + # shadow_measurements, + # endpoint="anvil", + # user_endpoint_config={"account": "MY_ANVIL_ACCOUNT"}, + # ) + return result + + +async def main(): + """Launch agent and demonstrate season prediction.""" + async with await Manager.from_exchange_factory( + factory=LocalExchangeFactory(), + executors=ThreadPoolExecutor(), + ) as manager: + # Launch the agent + agent = await manager.launch(GroundhogAgent) + + # Generate simulated shadow measurements (Phil's methodology is, of course, proprietary) + shadow_measurements = [round(random.uniform(1.0, 4.0), 1) for _ in range(10)] + print(f"Shadow measurements: {shadow_measurements}\n") + result = await agent.predict(shadow_measurements) + + print(f"\n Prediction: {result['prediction'].upper()}🌤️🦫") + print(f" Shadow index: {result['shadow_index']}") + print(f" Confidence: {result['confidence']}") + + # Shutdown + await manager.shutdown(agent, blocking=True) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/academy/compute.py b/examples/academy/compute.py new file mode 100644 index 0000000..3136753 --- /dev/null +++ b/examples/academy/compute.py @@ -0,0 +1,39 @@ +# /// script +# requires-python = ">=3.12,<3.13" +# dependencies = ["numpy"] +# +# [tool.uv] +# exclude-newer = "2026-02-06T20:15:45Z" +# python-preference = "managed" +# +# [tool.hog.anvil] # Anvil Multi-User Globus Compute Endpoint +# endpoint = "5aafb4c1-27b2-40d8-a038-a0277611868f" +# account = "cis250461" # Type: string +# walltime = "00:05:00" # Type: string +# /// + +import groundhog_hpc as hog + + +@hog.function() +def predict_season(shadow_measurements: list[float]) -> dict[str, str | float]: + """Predict whether winter continues based on shadow measurements. + + This function runs in an isolated environment with numpy automatically installed. + Can be called with .local() for subprocess isolation or .remote() for HPC + execution via Globus Compute. + """ + import numpy as np + + arr = np.array(shadow_measurements) + + # Compute shadow index using numpy (requires the dependency!) + shadow_index = float(np.mean(arr) * np.std(arr) + np.sum(arr) * 0.01) + + prediction = "more winter" if shadow_index > 2.5 else "early spring" + + return { + "prediction": prediction, + "shadow_index": round(shadow_index, 2), + "confidence": "high" if abs(shadow_index - 2.5) > 0.5 else "extremely high", + } From d49ad086de9360f11356e458003fd46fc48fef4a Mon Sep 17 00:00:00 2001 From: Owen Price Skelly <21372141+OwenPriceSkelly@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:08:12 -0600 Subject: [PATCH 2/4] add academy integration to docs --- docs/examples/academy.md | 182 +++++++++++++++++++++++++++++++++++++++ docs/examples/index.md | 6 ++ mkdocs.yml | 1 + 3 files changed, 189 insertions(+) create mode 100644 docs/examples/academy.md diff --git a/docs/examples/academy.md b/docs/examples/academy.md new file mode 100644 index 0000000..b7e555c --- /dev/null +++ b/docs/examples/academy.md @@ -0,0 +1,182 @@ +# Integration: Academy + +Use Groundhog functions within [Academy](https://github.com/academy-agents/academy) agents for isolated compute with automatic dependency management. + +## The Groundhog Function + +```python title="compute.py" +# /// script +# requires-python = ">=3.12,<3.13" +# dependencies = ["numpy"] (1) +# +# [tool.uv] +# exclude-newer = "2026-02-06T20:15:45Z" +# python-preference = "managed" +# +# [tool.hog.anvil] # Anvil Multi-User Globus Compute Endpoint +# endpoint = "5aafb4c1-27b2-40d8-a038-a0277611868f" +# account = "cis250461" +# walltime = "00:05:00" +# /// + +import groundhog_hpc as hog + + +@hog.function() +def predict_season(shadow_measurements: list[float]) -> dict[str, str | float]: + """Predict whether winter continues based on shadow measurements. + + This function runs in an isolated environment with numpy automatically installed. + Can be called with .local() for subprocess isolation or .remote() for HPC + execution via Globus Compute. + """ + import numpy as np + + arr = np.array(shadow_measurements) + + # Compute shadow index using numpy (requires the dependency!) + shadow_index = float(np.mean(arr) * np.std(arr) + np.sum(arr) * 0.01) + + prediction = "more winter" if shadow_index > 2.5 else "early spring" + + return { + "prediction": prediction, + "shadow_index": round(shadow_index, 2), + "confidence": "high" if abs(shadow_index - 2.5) > 0.5 else "extremely high", + } +``` + +1. Numpy declared in PEP 723 metadata for the groundhog script, but not required in the agent environment. + +## The Academy Agent + +```python title="agent.py" +"""Academy agent that uses Groundhog for isolated compute with dependencies. + +This example demonstrates how Academy agents can leverage Groundhog to execute +functions requiring packages not in the agent's environment (numpy in this case). + +Key benefits: +- Agent environment stays clean (only academy-py + groundhog-hpc needed) +- Compute dependencies isolated in subprocess via .local() +- Same code can dispatch to Globus Compute endpoints via .remote() + +Run: uv run --with academy-py --with groundhog-hpc agent.py +""" + +import asyncio +import random + +from academy.agent import Agent, action +from academy.exchange import LocalExchangeFactory +from academy.manager import Manager +from concurrent.futures import ThreadPoolExecutor + +import groundhog_hpc # (1)! + +from compute import predict_season + + +class GroundhogAgent(Agent): + """Agent that uses an environment-sensitive scientific instrument (🌤️🦫🕳️) to predict the weather.""" + + @action + async def predict(self, shadow_measurements: list[float]) -> dict[str, str | float]: + """Make a groundhog-backed inference. + + Demonstrates groundhog's dependency isolation in an agentic context: + - Direct call fails (numpy not in agent environment) + - .local() succeeds (Groundhog manages isolated subprocess with numpy) + - Could use .remote() to dispatch to HPC clusters via Globus Compute + """ + # First, try calling directly - this will fail since numpy isn't available + print("Attempting direct call (will fail - numpy not in environment)...") + try: + result = predict_season(shadow_measurements) # (2)! + except ImportError as e: + print(f"Direct call failed (as expected): {e}") + + # Now use Groundhog's .local() for isolated execution with dependencies + print("\nUsing Groundhog .local() (succeeds - isolated subprocess)...") + result = predict_season.local(shadow_measurements) # (3)! + print(f".local() call succeeded!") + + # Or, dispatch to a Globus Compute endpoint + # result = predict_season.remote( + # shadow_measurements, + # endpoint="anvil", + # user_endpoint_config={"account": "MY_ANVIL_ACCOUNT"}, + # ) + return result + + +async def main(): + """Launch agent and demonstrate season prediction.""" + async with await Manager.from_exchange_factory( + factory=LocalExchangeFactory(), + executors=ThreadPoolExecutor(), + ) as manager: + # Launch the agent + agent = await manager.launch(GroundhogAgent) + + # Generate simulated shadow measurements (Phil's methodology is, of course, proprietary) + shadow_measurements = [round(random.uniform(1.0, 4.0), 1) for _ in range(10)] + print(f"Shadow measurements: {shadow_measurements}\n") + result = await agent.predict(shadow_measurements) + + print(f"\n Prediction: {result['prediction'].upper()}🌤️🦫") + print(f" Shadow index: {result['shadow_index']}") + print(f" Confidence: {result['confidence']}") + + # Shutdown + await manager.shutdown(agent, blocking=True) + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +1. See [Importing Groundhog Functions](imported_function.md) for details. +2. A direct call to `predict_season` fails, because numpy isn't installed in the agent environment +3. ... but `predict_season.local()` runs the function in an isolated subprocess, where numpy is installed automatically + +## Why Use Groundhog with Academy? + +Academy agents coordinate distributed workflows. Groundhog handles isolated compute with dependencies: + +- **Clean agent environment**: Only `academy-py` + `groundhog-hpc` needed +- **Dependency isolation**: Functions bring their own packages via PEP 723 metadata +- **Portability**: Same code works locally or on HPC clusters + +See [Importing Groundhog Functions](imported_function.md) for the full mechanics of importing and calling Groundhog functions from external scripts. + +## Running the Example + +```bash +uv run --with academy-py --with groundhog-hpc examples/academy/agent.py +``` + +Expected output: + +``` +Shadow measurements: [3.9, 2.9, 4.0, 1.1, 2.4, 2.6, 1.3, 2.4, 3.8, 1.2] + +Attempting direct call (will fail - numpy not in environment)... +Direct call failed (as expected): No module named 'numpy' + +Using Groundhog .local() (succeeds - isolated subprocess)... +[local] Installed 1 package in 33ms +.local() call succeeded! + + Prediction: MORE WINTER🌤️🦫 + Shadow index: 2.96 + Confidence: extremely high +``` + +For remote execution on HPC via Globus Compute, replace `.local()` with `.remote(endpoint="my_cluster")`. + +## Next Steps + +- **[Importing Groundhog Functions](imported_function.md)** - Details on import safety and calling Groundhog functions from external code +- **[Running Locally](local.md)** - More on `.local()` for subprocess isolation +- **[Academy Documentation](https://docs.academy-agents.org/)** - Academy official docs diff --git a/docs/examples/index.md b/docs/examples/index.md index 0a20d2d..572b8e8 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -21,6 +21,12 @@ Examples showing how to handle typical workflows: - **[PyTorch from Custom Sources](pytorch_custom_index.md)** - Configuring uv to install packages from cluster-specific indexes, local paths, or internal mirrors - **[Importing Groundhog Functions](imported_function.md)** - Calling Groundhog functions from regular Python scripts, REPLs, and notebooks (includes import safety and `mark_import_safe()`) +## Integration Examples + +Examples showing how to use Groundhog with other frameworks: + +- **[Academy Integration](academy.md)** - Using Groundhog functions within Academy agents for isolated compute with dependency management + ## Running the Examples All examples in this section are available in the [examples directory](https://github.com/Garden-AI/groundhog/tree/main/examples) of the Groundhog repository, and should be runnable with minimal modification (e.g. configuring your own endpoint/account etc) diff --git a/mkdocs.yml b/mkdocs.yml index f24f58b..007bd1d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -105,6 +105,7 @@ nav: - Endpoint Configuration: examples/configuration.md - Customizing Package Sources: examples/pytorch_custom_index.md - Importing Groundhog Functions: examples/imported_function.md + - Academy Integration: examples/academy.md - Concepts: - Functions and Harnesses: concepts/functions-and-harnesses.md - PEP 723 Metadata: concepts/pep723.md From fea52633469748a72ea933fc342bccc1321cf51c Mon Sep 17 00:00:00 2001 From: Owen Price Skelly <21372141+OwenPriceSkelly@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:49:58 -0600 Subject: [PATCH 3/4] fix linter issues --- examples/academy/agent.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/academy/agent.py b/examples/academy/agent.py index f59810a..c5a430f 100644 --- a/examples/academy/agent.py +++ b/examples/academy/agent.py @@ -13,15 +13,14 @@ import asyncio import random +from concurrent.futures import ThreadPoolExecutor from academy.agent import Agent, action from academy.exchange import LocalExchangeFactory from academy.manager import Manager -from concurrent.futures import ThreadPoolExecutor - -import groundhog_hpc # noqa: F401 -from compute import predict_season +import groundhog_hpc # noqa +from compute import predict_season # noqa class GroundhogAgent(Agent): @@ -46,7 +45,7 @@ async def predict(self, shadow_measurements: list[float]) -> dict[str, str | flo # Now use Groundhog's .local() for isolated execution with dependencies print("\nUsing Groundhog .local() (succeeds - isolated subprocess)...") result = predict_season.local(shadow_measurements) - print(f".local() call succeeded!") + print(".local() call succeeded!") # Or, dispatch to a Globus Compute endpoint # result = predict_season.remote( From 87e7ce80cbacb5e642049bdd1bd5891dadb8eea2 Mon Sep 17 00:00:00 2001 From: Owen Price Skelly <21372141+OwenPriceSkelly@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:52:43 -0600 Subject: [PATCH 4/4] appease ruff --- examples/academy/agent.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/academy/agent.py b/examples/academy/agent.py index c5a430f..dc2f590 100644 --- a/examples/academy/agent.py +++ b/examples/academy/agent.py @@ -11,6 +11,8 @@ Run: uv run --with academy-py --with groundhog-hpc agent.py """ +# ruff: noqa: I001, F401 +# import asyncio import random from concurrent.futures import ThreadPoolExecutor @@ -19,8 +21,8 @@ from academy.exchange import LocalExchangeFactory from academy.manager import Manager -import groundhog_hpc # noqa -from compute import predict_season # noqa +import groundhog_hpc +from compute import predict_season class GroundhogAgent(Agent):