HearthNet-Nemotron / hearthnet /ui /tabs /getting_started.py
GitHub Actions
feat: P0/P1 — Image, OCR, Translation tabs + styled headers on all tabs
b132c6e
Raw
History Blame
14.1 kB
"""Getting Started tab — node setup, deployment options, distribution guide."""
from __future__ import annotations
def build_getting_started_tab():
import gradio as gr
with gr.Column():
gr.HTML("""
<div style="background:linear-gradient(135deg,#1e1b4b,#312e81);
border-radius:10px;padding:16px 20px;margin-bottom:8px;
border:1px solid #4f46e5">
<h3 style="color:#fff;margin:0">🚀 Getting Started with HearthNet</h3>
<p style="color:rgba(255,255,255,.7);margin:4px 0 0;font-size:.85em">
Community AI mesh · setup guide · architecture overview · quick start
</p>
</div>
""")
gr.Markdown("""### Getting Started with HearthNet
HearthNet is a **local-first community AI mesh**. Each participant runs a node
on their own hardware. Nodes discover each other automatically and share AI
capabilities, files, and community posts — no central server required.
---
## Quick Start (any device with Python)
```bash
# 1. Clone the repo (PyPI package coming soon — use git clone for now)
git clone https://huggingface.co/spaces/build-small-hackathon/HearthNet
cd HearthNet
pip install -e .
# 2. Run your local node
python -m hearthnet.cli run
# 3. Open the UI
# http://localhost:7860
```
The **HF Space** above is the public demo — single node, SmolLM2-135M, no real peer mesh.
A **local install** gives you Ollama/llama.cpp models, real peer discovery, file sharing, and chat.
---
## What Works Where
| Feature | HF Space | Local Node |
|---------|----------|------------|
| Ask / LLM chat | SmolLM2-135M | Ollama / llama.cpp / any HF model |
| RAG (knowledge base) | pre-seeded corpus | upload your own docs |
| Direct messaging (Chat) | single-node only | real delivery to peers |
| Mesh topology graph | no peers on Space | live SVG with all discovered peers |
| Marketplace posts | single-node | replicated across mesh |
| File sharing (blobs) | local only | content-addressed peer transfer |
| Emergency mode | 30s probe | 30s probe |
| MoE expert routing | disabled | routes queries to best node |
| BitTorrent model weights | disabled | pull GGUF / safetensors from peer |
| Plant identification | unavailable | Florence-2 vision + LLM parse |
---
## Setting Up a Second Node
**Option A — Same LAN (automatic)**
```bash
# On any other device on the same Wi-Fi:
git clone https://huggingface.co/spaces/build-small-hackathon/HearthNet
cd HearthNet && pip install -e .
python -m hearthnet.cli run
# Both nodes see each other within ~5 seconds (mDNS + UDP broadcast)
```
**Option B — Different network (invite link)**
1. Open Settings → Join This Mesh → Generate Invite QR
2. Share the link or scan QR on the new device
3. `python -m hearthnet.cli invite redeem <link>`
**Option C — Raspberry Pi**
```bash
# Raspbian / any ARM Linux:
git clone https://huggingface.co/spaces/build-small-hackathon/HearthNet
cd HearthNet && pip install -e .
python -m hearthnet.cli run --host 0.0.0.0 --port 7860
# Access from phone/laptop: http://raspberry-pi-ip:7860
```
---
## MoE Expert Routing (Phase 3 — M27)
Each node in the mesh can advertise itself as an **expert** in certain topics.
When a query arrives, `moe.route` scores all known experts and returns the best match.
```python
import asyncio
from hearthnet.node import HearthNode
node = HearthNode("medical-pi", "Medical Node", "ed25519:community")
node.install_services(corpus="medical")
# Advertise this node as a medical expert
asyncio.run(node.bus.call("moe.register", (1, 0), {
"input": {
"expert_id": f"model:{node.node_id}",
"expert_type": "model",
"topic_tags": ["first_aid", "medication", "triage", "medical"],
"confidence_score": 0.85,
"community_id": "ed25519:community",
"name": "Medical Node",
"ttl_seconds": 3600,
}
}))
# Another node routes a query to the best expert:
result = asyncio.run(node.bus.call("moe.route", (1, 0), {
"input": {"query": "what is the dosage for ibuprofen?", "top_k": 3}
}))
# {"output": {"candidates": [{"expert_id": "model:medical-pi", "score": 0.92, ...}]}}
```
**Expert types**: `model` (LLM node), `service` (OCR/translation node),
`human` (on-call person), `external` (public API opt-in).
---
## BitTorrent-Style Model Sharing (Phase 3 — M26)
Nodes advertise which model weight files they hold. Peers can pull models
chunk-by-chunk using content-addressed transfer (BLAKE3 CID).
This is analogous to BitTorrent but peer-to-peer over the HearthNet transport.
```python
# On Node A (has llama3.2-3b-q4.gguf):
# ModelDistributionService auto-scans ~/.ollama/models and your models_dir
# It registers as model.advertise, model.list, model.chunk_read automatically
# On Node B (wants the model):
result = await node.bus.call("model.pull", (1, 0), {
"input": {
"model_name": "llama3.2:3b",
"source_node": "node-a-id", # node_id of the provider
"dest_dir": "~/.hearthnet/models", # optional; defaults to ~/.hearthnet/models
}
})
job_id = result["output"]["job_id"]
# Poll progress:
status = await node.bus.call("model.status", (1, 0), {
"input": {"job_id": job_id}
})
# {"output": {"progress": 0.42, "received_chunks": 84, "total_chunks": 200, ...}}
```
Files are saved to `~/.hearthnet/blobs/` (BLAKE3 CID-addressed) and
optionally installed into Ollama if available.
**CLI shortcut:**
```bash
python -m hearthnet.cli call model.list 1 0 '{}'
python -m hearthnet.cli call model.pull 1 0 '{"model_name":"llama3.2:3b","source_node":"node-a"}'
```
---
## Plant Identification Tool (M21 tool calls)
The `tool.plant_identify` capability identifies plants from images.
```python
import base64
# Load any JPEG/PNG image
with open("plant.jpg", "rb") as f:
img_b64 = base64.b64encode(f.read()).decode()
result = await node.bus.call("tool.plant_identify", (1, 0), {
"input": {
"image_b64": img_b64,
"hints": ["northern Europe", "found near water", "July"],
}
})
# {
# "name": "Urtica dioica",
# "common_name": "Stinging Nettle",
# "confidence": 0.81,
# "family": "Urticaceae",
# "is_toxic": false,
# "edible_parts": ["young leaves (cooked)"],
# "care_tips": ["wear gloves when handling", "boiling removes sting"],
# "backend_used": "local_vision"
# }
```
**Backend priority:**
1. **Local vision** — Florence-2 via `vision.describe` + LLM parse (no internet)
2. **HF Inference API** — set `HEARTHNET_HF_TOKEN` to enable (requires internet)
3. **Unavailable** — structured error with setup instructions
**With LLM tool calls (M21):**
```python
from hearthnet.services.llm.tools import ToolExecutor
from hearthnet.services.tools.plant import PLANT_TOOL_DEFINITION
executor = ToolExecutor(bus=node.bus, tools=[PLANT_TOOL_DEFINITION])
# Pass executor to LlmService — the LLM can now call plant_identify mid-generation
```
---
## Adding a Specialized Node
Each node only needs to register the capabilities it has hardware for:
```python
from hearthnet.node import HearthNode
from hearthnet.services.ocr import OcrService # Tesseract / TrOCR
node = HearthNode("ocr-pi", "Scanner Pi", "ed25519:community")
node.install_services()
node.bus.register_service(OcrService())
node.start()
# Now ANY node in the mesh can call bus.call("ocr.extract", ...)
# and this Pi answers it automatically
```
Other specialized node patterns:
- **Medical RAG node**: `RagService(corpus="medical")` + large medical embedding model
- **Translation node**: `TranslationService()` with NLLB-200 for low-resource languages
- **LoRa beacon node**: `LoraBeaconService(serial_port="/dev/ttyUSB0")` for 868 MHz offline heartbeats
- **Thin client**: No services installed — only routes requests to other nodes
---
## Distribution Options
| Method | Best for |
|--------|----------|
| `pip install -e .` | Development, Raspberry Pi, servers |
| `pip install hearthnet` | Once published to PyPI (coming soon) |
| **Browser (PWA)** | Any device — open `http://node-ip:7860`. Add to home screen. |
| **Docker** | Servers: `docker build -t hearthnet . && docker run -p 7860:7860 hearthnet` |
| **Android app** | Browser to a local node; native app planned (M22) |
| **Relay node** | One node with public IP acts as relay (M15); remote nodes connect through it |
---
## Testing Your Setup
```bash
# All unit tests (102 tests, 0 failures):
pytest tests/ -q
# Skip E2E (Playwright) tests:
pytest tests/ -q --ignore=tests/test_e2e_user_stories.py
# Two-node local demo:
python -m scripts.demo_two_nodes
# Test MoE routing:
python -c "
from hearthnet.node import HearthNode
import asyncio
node = HearthNode('test', 'Test', 'ed25519:demo')
node.install_demo_services()
async def main():
# Register a demo expert
await node.bus.call('moe.register', (1, 0), {'input': {
'expert_id': 'model:test', 'expert_type': 'model',
'topic_tags': ['first_aid','emergency'], 'confidence_score': 0.9,
'community_id': 'ed25519:demo'
}})
result = await node.bus.call('moe.route', (1, 0), {'input': {'query': 'emergency first aid'}})
print(result['output'])
asyncio.run(main())
"
```
---
## Calling a Capability on Any Node
Every feature in HearthNet is a **named capability** on the bus. Calling one is always the same pattern:
```python
import asyncio
from hearthnet.node import HearthNode
node = HearthNode("my-node", "My Node", "ed25519:community")
node.install_demo_services() # registers llm.chat, rag.query, chat.send, etc.
async def main():
# --- LLM chat ---
result = await node.bus.call("llm.chat", (1, 0), {
"params": {}, # {} = let the bus pick the best node
"input": {
"messages": [
{"role": "user", "content": "What is HearthNet?"}
]
}
})
print(result["output"]["message"]["content"])
# --- RAG query ---
result = await node.bus.call("rag.query", (1, 0), {
"params": {"corpus": "community"}, # route to node with this corpus
"input": {"query": "emergency water purification", "k": 3}
})
for chunk in result["output"]["chunks"]:
print(chunk["text"][:80])
# --- Send a chat message ---
result = await node.bus.call("chat.send", (1, 0), {
"input": {"recipient": "bob-node-id", "body": "Hello Bob!"}
})
print(result["output"]["delivered"]) # "queued" or "direct"
# --- List marketplace posts ---
result = await node.bus.call("market.list", (1, 0), {"input": {}})
for post in result["output"]["posts"]:
print(f"{post['category']}: {post['title']}")
# --- Discover available capabilities ---
entries = list(node.bus.registry.all())
for e in entries:
print(f" {e.descriptor.name}@{e.descriptor.version[0]}.{e.descriptor.version[1]}"
f" on {e.node_id} params={e.descriptor.params}")
asyncio.run(main())
```
**From the CLI (no Python required):**
```bash
# Call any capability from the command line
python -m hearthnet.cli call llm.chat 1 0 \\
'{"input":{"messages":[{"role":"user","content":"Hello!"}]}}'
python -m hearthnet.cli call rag.query 1 0 \\
'{"params":{"corpus":"community"},"input":{"query":"emergency water","k":3}}'
python -m hearthnet.cli capabilities # list all available capabilities
```
---
## Getting Model Weights from a Peer Node
A node **without internet** can pull model weights from any peer that has them.
The weights travel as BLAKE3 content-addressed chunks over the HearthNet transport
(no BitTorrent tracker needed — peers are already known from the mesh):
```python
# Step 1: Find what models a peer has
models = await node.bus.call("model.list", (1, 0), {"input": {}})
for m in models["output"]["models"]:
print(f" {m['name']} ({m['size_bytes'] // 1024**2} MB) on {m['node_id']}")
# Step 2: Pull a model from a specific peer
job = await node.bus.call("model.pull", (1, 0), {
"input": {
"model_name": "llama3.2:3b", # name as reported by model.list
"source_node": "peer-node-id", # node_id from the list above
# "dest_dir": "/custom/path" # optional; default: ~/.hearthnet/blobs/
}
})
job_id = job["output"]["job_id"]
# Step 3: Poll until complete
import asyncio
while True:
status = await node.bus.call("model.status", (1, 0), {"input": {"job_id": job_id}})
pct = status["output"]["progress"] * 100
print(f" {pct:.0f}% — {status['output']['state']}")
if status["output"]["state"] in ("complete", "error"):
break
await asyncio.sleep(2)
```
**Notes:**
- Offline nodes can pull from any reachable peer — no internet needed, only LAN
- Files land in `~/.hearthnet/blobs/` (BLAKE3 CID-addressed, never duplicated)
- If Ollama is installed, the model is automatically registered after download
- On HF Space: model.pull works peer-to-peer but the Space has no persistent storage
---
## Connecting Your Local Node to the HF Space
The HF Space is a live single-node HearthNet instance. You can connect your
local node to it and use its SmolLM2-135M or share your local Ollama models
with it:
```bash
# 1. Redeem the HF Space invite
python -m hearthnet.cli invite redeem \\
"hnvite://v1/hf-space-1c95381d?host=build-small-hackathon-hearthnet.hf.space&port=443&transport=https&level=member"
# 2. Verify peer was added
python -m hearthnet.cli peers
# hf-space-1c95381d build-small-hackathon-hearthnet.hf.space:443 [llm.chat, rag.query, ...]
# 3. Route a query — if your Ollama is faster, it answers instead of the Space
python -m hearthnet.cli call llm.chat 1 0 \\
'{"input":{"messages":[{"role":"user","content":"Hello from the mesh!"}]}}'
```
Or use the connect script (checks both sides):
```bash
python scripts/connect_to_hf.py
```
**What happens after connecting:**
- Your local LLM (if faster/better) will be preferred over the Space's SmolLM2
- Your local RAG corpus is accessible to Space users who query `rag.query`
- Emergency alerts propagate to both the Space and your local node
- Marketplace posts replicate between your node and the Space
""")