Skip to content

Add CardioPulse — live cardiovascular fitness-test agent (BLE + ASI:One) - #153

Open
ishneet42 wants to merge 4 commits into
fetchai:mainfrom
ishneet42:add-cardiopulse-agent
Open

Add CardioPulse — live cardiovascular fitness-test agent (BLE + ASI:One)#153
ishneet42 wants to merge 4 commits into
fetchai:mainfrom
ishneet42:add-cardiopulse-agent

Conversation

@ishneet42

@ishneet42 ishneet42 commented Jun 12, 2026

Copy link
Copy Markdown

Summary

Adds CardioPulse under contributors/cardiopulse-agent/ — a live, stream-driven agent that reads a Garmin watch's heart rate over Bluetooth and runs a timed three-phase autonomic test (resting baseline → orthostatic → paced breathing) entirely through an ASI:One chat. It returns an estimated Cardio Fitness Age with reference ranges, an inline HR-timeline chart, and an ASI-1 coaching summary; repeated runs build a per-user trend.

Two-agent design: a local bridge agent subscribes to the watch's BLE Heart Rate characteristic (Bleak) and POSTs each reading to the main agent, which is registered on Agentverse via mailbox and reachable through ASI:One.

Scope: a proof of concept for a live, stream-driven agent — not a medical device and not health advice. Readings are illustrative estimates (~±5 years); the trend across repeated tests matters more than any single number.

Type of Change

  • New agent example
  • Bug fix
  • Documentation update
  • Refactor / cleanup
  • Other

Checklist

  • I have starred this repository.
  • New community agents are under contributors/<agent-name>/ (not repo root).
  • I ran ruff check ..
  • I ran ruff format ..
  • I added/updated README.md for changed example(s).
  • I added .env.example if environment variables are required.
  • I added demo image/GIF (if applicable).
  • I added agent profile link (if applicable). (The agent address is derived per-deployment from AGENT_SEED; the README documents how to deploy via mailbox.)
  • I updated contributors/CHANGELOG.md for community agent changes.
  • I added my agent to the Community Contributors table in root README.md.
  • I verified paths/commands used in docs.
  • I understand this PR requires maintainer review before merge (review-required CI).

Related Issue

None.

Notes for Reviewers

  • The agent needs local hardware — a Garmin watch broadcasting heart rate over BLE — so the live path can't run headless in CI. test_scoring.py exercises the scoring engine end-to-end with synthetic HR data and runs with no watch (python test_scoring.py).
  • The bridge → agent link uses a localhost REST POST rather than agent-to-agent messaging, because Agentverse mailbox routing is too slow for the 1 Hz heart-rate stream; the mailbox carries the ASI:One conversation. Both agents run on the same machine. This is documented in the agent README's Architecture section.
  • No secrets or personal data are committed; .env.example documents all configuration.

ishneet42 and others added 3 commits June 12, 2026 13:13
A two-agent example under contributors/cardiopulse-agent/ that streams a Garmin watch's heart rate over BLE and runs a three-phase autonomic test (resting baseline, orthostatic, paced breathing) through ASI:One, returning an estimated Cardio Fitness Age with reference ranges, an inline HR chart, and an ASI-1 coaching summary. A local bridge agent reads the watch over BLE and POSTs readings to the mailbox-registered main agent.

Proof of concept for a live, stream-driven agent — not a medical device.
mypy inferred the mixed-value _stats dict as dict[str, object], making the counter increments and comparison invalid. Split the int counters from the last-error string into separate, single-typed containers.
@gautammanak1 gautammanak1 self-assigned this Jun 16, 2026
@gautammanak1
gautammanak1 self-requested a review June 16, 2026 11:54
@gautammanak1

Copy link
Copy Markdown
Collaborator

No description provided.

@gautammanak1

gautammanak1 commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

CardioPulse Agent Review

Summary

This PR adds CardioPulse, a well-architected cardiovascular fitness test agent that reads heart rate from Garmin watches over BLE and runs a 3-phase autonomic test. The codebase is clean, modular, and follows Python best practices. The two-agent design (bridge + main agent) is a smart solution to the 1Hz streaming latency problem with Agentverse mailbox routing.

Risk level: Low — this is a proof-of-concept health demo with clear disclaimers, not a medical device. The code is production-quality for its intended purpose.


What I Liked

  • Excellent separation of concerns: agent.py, bridge_agent.py, chat_handler.py, scoring.py, chart.py, coach.py, history.py, session_state.py, models.py, image_host.py, and diagnose_ble.py each have a single, clear responsibility.
  • Smart architecture decision: Using localhost REST POST for the 1Hz BLE stream instead of agent-to-agent messaging is the right call given Agentverse mailbox latency. This is well-documented in the code.
  • Thread-safe shared state: session_state.py properly uses threading.Lock around the deque buffers for concurrent access from the REST endpoint and chat handler.
  • Graceful error handling: The bridge agent's _ble_loop never exits on failure — it just logs and retries, which is perfect for a KeepAlive-managed service.
  • Data quality awareness: scoring.py detects bad baselines (high RHR, insufficient samples) and surfaces warnings rather than silently producing alarming scores.
  • User-scoped history: history.py uses sender to scope test records per user, preventing cross-user data leakage.
  • Comprehensive offline testing: test_scoring.py lets developers verify scoring logic without hardware.
  • Helpful diagnostic tool: diagnose_ble.py is a great UX addition for troubleshooting BLE connectivity.

Critical Issues

contributors/cardiopulse-agent/scoring.py:126 — Undefined variable code

elif rhr < 60:
    code -= 3  # ❌ 'code' is undefined

This should be adjust -= 3. The current code will raise a NameError when processing a test with RHR between 60-64 bpm.

Fix:

elif rhr < 60:
    adjust -= 3

contributors/cardiopulse-agent/scoring.py:144 — Indentation error

 else:
    verdict = (
        "Your cardiovascular function is roughly aligned with your "
        "chronological age."
    )

The else: clause has an extra leading space, causing an IndentationError. This is a syntax error that will prevent the module from loading.

Fix:

else:
    verdict = (
        "Your cardiovascular function is roughly aligned with your "
        "chronological age."
    )

contributors/cardiopulse-agent/scoring.py:157 — Undefined variable error

if rhr >= 90:
    error.append(  # ❌ 'error' is undefined
        "Your baseline HR was unusually high for seated rest — stress, "
        "caffeine, talking, or moving around just before the test commonly "
        "inflate it. Treat this score as a low-confidence reading and "
        "re-test when you're calm and have been seated for 10+ minutes."
    )

This should be notes.append(...). The current code will raise a NameError when scoring a test with RHR ≥ 90 bpm.

Fix:

if rhr >= 90:
    notes.append(
        "Your baseline HR was unusually high for seated rest — stress, "
        "caffeine, talking, or moving around just before the test commonly "
        "inflate it. Treat this score as a low-confidence reading and "
        "re-test when you're calm and have been seated for 10+ minutes."
    )

Correctness / Logic

contributors/cardiopulse-agent/test_scoring.py:44 — Indentation error

 if i < 5:
    out.append(rhr + (peak_delta * i // 5))

The if statement has an extra leading space. This is a syntax error that will prevent test_scoring.py from running.

Fix:

if i < 5:
    out.append(rhr + (peak_delta * i // 5))

Security

No security concerns identified:

  • No hardcoded credentials or secrets
  • Proper use of environment variables for sensitive configuration (AGENT_SEED, BRIDGE_SEED, ASI1_API_KEY, IMGUR_CLIENT_ID)
  • The .env.example pattern is correctly documented
  • No SQL/command injection vectors
  • The localhost REST endpoint has no authentication, but this is acceptable for a same-machine proof-of-concept

Performance

The code is well-optimized for its use case:

  • Queue-based buffering: The bridge agent uses asyncio.Queue(maxsize=200) to decouple BLE callbacks from HTTP POSTs, preventing blocking.
  • HTTP connection reuse: httpx.AsyncClient with keep-alive in _sender_loop avoids TCP handshake overhead for each reading.
  • Bounded deques: session_state.py uses deque(maxlen=900) and deque(maxlen=3000) to prevent unbounded memory growth.
  • Graceful degradation: Queue-full drops old readings rather than blocking the BLE callback, which is the right tradeoff for a 1Hz stream.

Code Quality

Style notes (non-blocking)

  • diagnose_ble.py uses print() extensively. This is acceptable for a diagnostic CLI tool, but consider using logging for consistency with the rest of the codebase.
  • test_scoring.py uses print() for output, which is fine for a test script.
  • bridge_agent.py:25 has a print() statement for startup feedback — acceptable for a user-facing CLI.

Tests

  • test_scoring.py provides good coverage of the scoring engine with synthetic data across different fitness profiles (fit, average, deconditioned).
  • The test exercises the core scoring.compute() function and validates the formatted output.
  • Consider adding edge case tests: empty arrays, single-element arrays, extreme HR values (e.g., 40 bpm, 200 bpm).

Suggested Fixes

Here are the fixes for the three critical issues in scoring.py:

# Line 126: Fix undefined variable
elif rhr < 60:
    adjust -= 3  # was: code -= 3

# Line 144: Fix indentation
else:  # was:  else:
    verdict = (
        "Your cardiovascular function is roughly aligned with your "
        "chronological age."
    )

# Line 157: Fix undefined variable
if rhr >= 90:
    notes.append(  # was: error.append(
        "Your baseline HR was unusually high for seated rest — stress, "
        "caffeine, talking, or moving around just before the test commonly "
        "inflate it. Treat this score as a low-confidence reading and "
        "re-test when you're calm and have been seated for 10+ minutes."
    )

Questions for the Author

  1. The AGENT_URL in bridge_agent.py defaults to http://127.0.0.1:8001/bpm. Should there be a health check endpoint on the main agent so the bridge can verify connectivity before streaming?
  2. The SESSIONS dict in chat_handler.py grows unbounded. For a long-running agent, should there be session cleanup (e.g., TTL-based eviction)?
  3. The catbox.moe image host is anonymous and public. Are there any privacy concerns about uploading user health charts to a third-party service?

Conclusion

This is a well-designed, well-documented addition to the innovation lab examples. The architecture is sound, the code is clean, and the health-focused use case is compelling. The three syntax errors in scoring.py and test_scoring.py are blockers that need to be fixed before merge, but they're straightforward typos.

Once the critical issues are resolved, this will be a valuable example of a stream-driven agent with real-world hardware integration.

Files reviewed:

  • contributors/cardiopulse-agent/agent.py
  • contributors/cardiopulse-agent/bridge_agent.py
  • contributors/cardiopulse-agent/chart.py
  • contributors/cardiopulse-agent/chat_handler.py
  • contributors/cardiopulse-agent/coach.py
  • contributors/cardiopulse-agent/diagnose_ble.py
  • contributors/cardiopulse-agent/history.py
  • contributors/cardiopulse-agent/image_host.py
  • contributors/cardiopulse-agent/models.py
  • contributors/cardiopulse-agent/scoring.py
  • contributors/cardiopulse-agent/session_state.py
  • contributors/cardiopulse-agent/test_scoring.py

if cardio_age < age - 2:
verdict = (
f"Your cardiovascular function is reading "
f"{age - cardio_age} years younger than your chronological age."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contributors/cardiopulse-agent/scoring.py:126 — Undefined variable code

The variable code is not defined. This should be adjust -= 3 to match the surrounding logic.

Suggested change
f"{age - cardio_age} years younger than your chronological age."
elif rhr < 60:
adjust -= 3

# confidence rather than silently produce a scary score.
notes: list[str] = []
if rhr >= 90:
notes.append(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contributors/cardiopulse-agent/scoring.py:144 — Indentation error

The else: clause has an extra leading space, causing an IndentationError. Remove the extra space.

Suggested change
notes.append(
else:
verdict = (
"Your cardiovascular function is roughly aligned with your "
"chronological age."
)

)
quality_note = " ".join(notes) if notes else None

return TestResult(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contributors/cardiopulse-agent/scoring.py:157 — Undefined variable error

The variable error is not defined. This should be notes.append(...) to match the list declared earlier in the function.

Suggested change
return TestResult(
if rhr >= 90:
notes.append(
"Your baseline HR was unusually high for seated rest — stress, "
"caffeine, talking, or moving around just before the test commonly "
"inflate it. Treat this score as a low-confidence reading and "
"re-test when you're calm and have been seated for 10+ minutes."
)

import math

return [int(rhr + swing * math.sin(2 * math.pi * i / 10)) for i in range(n)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contributors/cardiopulse-agent/test_scoring.py:44 — Indentation error

The if statement has an extra leading space, causing an IndentationError. Remove the extra space.

Suggested change
if i < 5:
out.append(rhr + (peak_delta * i // 5))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants