Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ innovation-lab-examples/
| Example | Description | Tech Stack | Difficulty |
|---------|-------------|------------|------------|
| [contributors/community_agent](contributors/community_agent/) | AI community growth agent for events and hackathons | Python, uAgents, ASI:One, Tavily | 🟡 Intermediate |
| [contributors/cardiopulse-agent](contributors/cardiopulse-agent/) | Live cardiovascular fitness test from a Garmin BLE heart-rate stream | Python, uAgents, Bleak, ASI:One | 🟡 Intermediate |

### 🌐 Web3 & Blockchain

Expand Down
4 changes: 3 additions & 1 deletion contributors/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Added
- `gemini-research-agent/`: Added Gemini-powered research assistant demonstrating the standard Agent Chat Protocol (@Kavurubuvanesh)
- `contributors/` folder and contribution guide for community agent examples
- `contributors/community_agent/` — moved from repository root; AI community growth agent for events and hackathons
- `contributors/community_agent/` — moved from repository root; AI community growth agent for events and hackathons
- `contributors/cardiopulse-agent/` — live cardiovascular fitness-test agent that streams a Garmin watch's heart rate over BLE and runs a three-phase test through ASI:One
- `contributors/community_agent/` — moved from repository root; AI community growth agent for events and hackathons
40 changes: 40 additions & 0 deletions contributors/cardiopulse-agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# ============================================================================
# CardioPulse — environment configuration
# Copy this file to `.env` and fill in the values. Never commit your `.env`.
# ============================================================================

# ---- Main CardioPulse agent -------------------------------------------------

# Seed phrase that deterministically generates the agent's address.
# Use any unique random string. Change this before deploying.
AGENT_SEED=change-me-to-a-unique-random-string

# ASI:One API key — powers the natural-language coaching summary after a test.
# Optional: without it the agent falls back to a built-in static summary and
# still works end to end. Get a key at https://asi1.ai
ASI1_API_KEY=your_asi1_api_key_here
ASI1_MODEL=asi1

# ---- Bridge agent (runs locally on the machine with the watch) --------------

# Seed phrase for the bridge agent. Use a different unique random string.
BRIDGE_SEED=change-me-to-a-different-unique-random-string

# Substring the bridge matches when scanning for your watch over BLE.
# A Garmin Forerunner advertises as e.g. "Forerunner 165 12345".
GARMIN_NAME=Forerunner

# ---- Optional ---------------------------------------------------------------

# Where the bridge POSTs heart-rate readings. Defaults to the main agent's
# local REST endpoint, which is correct when both run on the same machine.
# Only change this if you move the agent to a different host/port.
# AGENT_URL=http://127.0.0.1:8001/bpm

# Reserved for a future cloud-hosted setup where the bridge would message the
# agent by address instead of POSTing to localhost. Not used in local mode.
# CARDIOPULSE_ADDRESS=agent1q...

# Chart delivery works out of the box via catbox.moe (no key required).
# Optionally set an Imgur Client-ID to use Imgur as a fallback image host.
# IMGUR_CLIENT_ID=your_imgur_client_id_here
12 changes: 12 additions & 0 deletions contributors/cardiopulse-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
venv/
__pycache__/
*.pyc
.env
.DS_Store
*.log

# Per-user test history — personal data, never commit
data/

# Regenerated chart artifact (charts are produced in-memory at runtime)
assets/last_test.png
171 changes: 171 additions & 0 deletions contributors/cardiopulse-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# CardioPulse

A live cardiovascular fitness-test agent for Agentverse and ASI:One.

From a chat message, the agent reads your live heart rate from a Garmin watch
over Bluetooth, walks you through a short three-phase test, and returns an
estimated **Cardio Fitness Age** with reference ranges, an inline chart, and a
plain-language summary. Repeated runs build a per-user trend.

> **Scope / disclaimer:** This is a proof of concept for a *live, stream-driven
> agent* — one that ingests a real-time sensor feed, reasons on it, and acts,
> all inside a chat. It is **not a medical device and not health advice.** The
> readings are illustrative estimates (roughly ±5 years); the trend across
> repeated tests matters far more than any single number.

## Overview

- **Category:** Live data, BLE, agent-to-agent
- **Tech stack:** Python, uAgents, Bleak (BLE), ASI:One Agent Chat Protocol, ASI-1, matplotlib
- **Status:** demo

## Features

- Ingests a live 1 Hz heart-rate stream from a Garmin watch over standard BLE.
- Runs a timed three-phase autonomic protocol (resting baseline → orthostatic →
paced breathing) entirely through a chat conversation.
- Returns a structured result with reference ranges, an inline HR-timeline chart,
and an ASI-1 natural-language coaching summary.
- Persists results per user and renders a trend chart across repeated tests.
- Two-agent architecture: a local BLE bridge agent + a mailbox-registered main
agent reachable through ASI:One.

## Prerequisites

- **Python 3.12** (uAgents is incompatible with Python 3.14's asyncio changes)
- A **Garmin watch** with "Broadcast Heart Rate" (Forerunner, Fenix, Venu, …),
or any standard BLE heart-rate strap (e.g. Polar H10)
- An [Agentverse](https://agentverse.ai) account
- *(Optional)* an [ASI:One](https://asi1.ai) API key for the AI coaching summary

## Installation

```bash
git clone <your-fork-url>
cd contributors/cardiopulse-agent
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

## Environment Variables

```bash
cp .env.example .env
```

| Variable | Required | Purpose |
|---|---|---|
| `AGENT_SEED` | yes | Seed that generates the main agent's address |
| `BRIDGE_SEED` | yes | Seed for the bridge agent (use a different value) |
| `GARMIN_NAME` | yes | Substring matched when scanning for the watch over BLE |
| `ASI1_API_KEY` | optional | Enables the AI coaching summary (static fallback otherwise) |
| `AGENT_URL` | optional | Where the bridge POSTs readings (default `http://127.0.0.1:8001/bpm`) |
| `IMGUR_CLIENT_ID` | optional | Fallback image host; charts use catbox.moe by default |

## Run the Agent

Start the main agent first so its REST endpoint is up before the bridge POSTs to
it. Both run on the same machine.

```bash
# 1. Main agent (chat protocol + local /bpm endpoint)
python agent.py

# 2. On the watch: enable Broadcast Heart Rate and keep that screen active.

# 3. Bridge agent (BLE -> POST to the main agent), in a second terminal
python bridge_agent.py
```

Then chat with the agent via ASI:One (or the Agentverse Manual Test panel):

```
age 25, start test
```

> For a clean, deterministic session, use the **Manual Test** panel on the
> agent's Agentverse profile rather than the main ASI:One chat.

## Expected Output

```
User: age 27, start test
Agent: Got it — age 27. Starting the test now.
Agent: Connected. Latest BPM: 64.
Phase 1 — Resting baseline (2 minutes) ...
(3 minutes later)
Cardio Fitness Test — Results
Cardio Fitness Age: 24 (you are 27)
Resting HR: 58 bpm — a well-rested value for your age is typically under 65 bpm.
...
[inline HR-timeline chart]
Coach's read: ...
[trend chart, from the second test onward]
```

Commands the agent understands: `help`, `age N`, `start test`
(`age N, start test` does both), `status`.

## Demo

![CardioPulse demo](./assets/demo.png)

## Agent Profile

Deployed on Agentverse via mailbox (the address is derived from your
`AGENT_SEED`). See [agentverse.ai](https://agentverse.ai).

## Architecture

```
[ Garmin Forerunner ]
│ BLE — standard Heart Rate Service
[ bridge_agent.py ] ── HTTP POST (localhost:8001/bpm) ──► [ agent.py — CardioPulse ]
local uAgent uAgent: REST + mailbox
Agent Chat Protocol
(Agentverse mailbox)
[ ASI:One chat ]
```

- **Bridge agent** (`bridge_agent.py`) subscribes to the watch's BLE Heart Rate
Measurement characteristic via Bleak and POSTs each reading to the main
agent's local REST endpoint, reconnecting on its own if the watch drops.
- **Main agent** (`agent.py`) receives the stream on a local `/bpm` endpoint and
is registered on Agentverse via mailbox for the ASI:One chat. Both agents run
on the same machine, so the localhost POST keeps up with the 1 Hz stream —
agent-to-agent mailbox routing is too slow for that rate, which is why HR data
uses REST while the conversation uses the mailbox.
- Readings accumulate in a thread-safe in-memory ring buffer (`session_state`).
The chat handler runs the test as a sequential coroutine and samples HR
windows from that buffer at each phase boundary — the telemetry stream
populates state but does not drive control flow.

## Verifying scoring without a watch

```bash
python test_scoring.py
```

Generates synthetic HR data for several hypothetical users and runs the scoring
engine end to end — useful for sanity-checking formula changes. Use
`python diagnose_ble.py` to confirm the watch is visible over BLE.

## Troubleshooting

- **"I'm not receiving any heart rate data"** — the watch isn't broadcasting.
Enable Broadcast Heart Rate and keep that screen awake (tap occasionally).
- **Bridge can't find the watch** — make sure `GARMIN_NAME` matches what the
watch advertises, and grant the terminal/Python Bluetooth permission
(macOS: System Settings → Privacy & Security → Bluetooth).
- **`RuntimeError: no running event loop` on startup** — you're on Python 3.14;
recreate the venv with Python 3.12.
- **Agent not reachable on ASI:One** — confirm the agent process is running and
its mailbox is connected; the mailbox only exists while the agent is up.

## License

Apache-2.0 (repository default).
69 changes: 69 additions & 0 deletions contributors/cardiopulse-agent/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
CardioAgent: a live cardiovascular fitness test agent on Agentverse + ASI:One.

The agent guides users through a 3-minute test using live heart rate readings
that a companion `bridge_agent.py` POSTs to a local REST endpoint. (Agentverse
mailbox routing is too slow for 1Hz streaming, so the same-machine bridge uses
a direct localhost POST; the mailbox is used only for the ASI:One chat.)

Run:
python agent.py

Then, on the same machine, run the bridge agent (which talks to the Garmin
watch over BLE and POSTs readings to this agent):

python bridge_agent.py
"""

from __future__ import annotations

import os

from dotenv import load_dotenv

# Load env vars BEFORE importing modules that read them at import time.
load_dotenv()

from uagents import Agent, Context # noqa: E402

from chat_handler import chat_proto # noqa: E402
from models import HRAck, HRReading # noqa: E402
import session_state # noqa: E402


AGENT_SEED = os.environ.get("AGENT_SEED", "cardio-agent-default-seed-change-me")


agent = Agent(
name="cardio-test-agent",
seed=AGENT_SEED,
port=8001,
mailbox=True,
)

agent.include(chat_proto, publish_manifest=True)


# Receive HR readings via a fast local REST endpoint instead of agent-to-agent
# messaging. Agent messaging routes via Agentverse mailbox which is too slow
# for 1Hz streaming data; the bridge runs on the same machine, so direct
# localhost POST is both faster and simpler.
@agent.on_rest_post("/bpm", HRReading, HRAck)
async def receive_bpm(ctx: Context, req: HRReading) -> HRAck:
"""REST endpoint that the local bridge POSTs heart rate readings to."""
session_state.receive_bpm(req.bpm, req.rr_intervals, req.ts)
return HRAck(ok=True)


@agent.on_event("startup")
async def on_startup(ctx: Context) -> None:
ctx.logger.info("CardioAgent online")
ctx.logger.info(f"Agent address: {agent.address}")
ctx.logger.info(
"Copy this address into your bridge_agent .env as CARDIOPULSE_ADDRESS, "
"then run `python bridge_agent.py` on the machine with the watch."
)


if __name__ == "__main__":
agent.run()
Binary file added contributors/cardiopulse-agent/assets/demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading