Spaces:
Running on Zero
Running on Zero
File size: 18,330 Bytes
8514223 e6bca78 8514223 66a1a95 8514223 f08047d 8514223 f08047d 8514223 e6bca78 66a1a95 e6bca78 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 | # HearthNet β HOWTO Guide
This document answers the most common setup and usage questions.
---
## Table of Contents
1. [Quick Start (single machine)](#1-quick-start)
2. [Raspberry Pi Setup](#2-raspberry-pi-setup)
3. [How Nodes Discover Each Other](#3-discovery)
4. [Connecting from a Second Device / Browser](#4-multi-device)
5. [Adding Content to the RAG Knowledge Base](#5-rag)
6. [Configuring LLM Backends](#6-llm-backends)
7. [Creating and Managing a Community](#7-community)
8. [Inviting Other Nodes](#8-inviting)
9. [How to Extend HearthNet (developer)](#9-extending)
10. [Troubleshooting](#10-troubleshooting)
11. [How Routing Works](#11-routing)
12. [Creating a Special-Feature Node](#12-special-feature-nodes)
---
## 1. Quick Start
```bash
# Install Python 3.11+
pip install -e ".[dev]"
# Start the Gradio UI (opens at http://127.0.0.1:7860)
python app.py
# Or via the CLI:
python -m hearthnet.cli run
```
The node starts with:
- mDNS announcement (LAN discovery)
- UDP multicast announcement (fallback)
- A local-only Gradio UI at http://127.0.0.1:7860
- Demo LLM (echo fallback until a real backend is configured)
---
## 2. Raspberry Pi Setup
HearthNet runs on a Raspberry Pi 4 (4 GB) or Pi 5.
### Recommended model for Pi
**MiniCPM3-4B** via Ollama or llama.cpp β fits in 4 GB RAM.
```bash
# 1. Install on Pi (Raspberry Pi OS 64-bit bookworm)
sudo apt update && sudo apt install python3-pip git -y
git clone https://github.com/HearthNet/hearthnet
cd hearthnet
pip install -e .
# 2. Install Ollama (optional but recommended)
curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen2.5:3b # ~2 GB, fast on Pi 5
# or
ollama pull minicpm3:4b # if available
# 3. Create config
mkdir -p ~/.hearthnet
cat > ~/.hearthnet/config.toml << 'EOF'
[identity]
auto_generate = true
[transport]
host = "0.0.0.0" # listen on all interfaces so LAN clients can connect
port = 7080
[discovery]
mdns_enabled = true
udp_enabled = true
[ui]
host = "0.0.0.0" # serve Gradio on all interfaces
port = 7860
[[llm.backends]]
name = "ollama"
url = "http://localhost:11434"
EOF
# 4. Run
python -m hearthnet.cli run
```
Open `http://<pi-ip>:7860` from any browser on the LAN.
### Auto-start on boot (systemd)
```ini
# /etc/systemd/system/hearthnet.service
[Unit]
Description=HearthNet Community AI
After=network.target
[Service]
User=pi
WorkingDirectory=/home/pi/hearthnet
ExecStart=/home/pi/.local/bin/python -m hearthnet.cli run
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl enable hearthnet
sudo systemctl start hearthnet
```
---
## 3. Discovery
HearthNet uses **three discovery methods** (in priority order):
### mDNS (LAN β automatic)
Every node announces itself as `_hearthnet._tcp.local.` using **Zeroconf**.
No configuration needed. Works on any LAN where mDNS is not blocked.
```
Node A starts β announces _hearthnet._tcp.local. via mDNS
Node B starts β discovers Node A, sees its capabilities, registers them on its bus
```
### UDP multicast (LAN β fallback)
Uses multicast group `239.255.42.42:42424`.
Works when mDNS is blocked by a firewall or managed switch.
### Relay tier (WAN β Phase 2)
For nodes behind NAT or across the internet, configure a relay URL:
```toml
[discovery]
relay_urls = ["https://your-relay.example.com"]
```
See [docs/p2_p3/M15-relay-tier.md](../p2_p3/M15-relay-tier.md).
### Checking connected peers
**In the UI:** Settings tab β "Connected Peers & Capabilities" β click Refresh.
**Via CLI:**
```bash
python -m hearthnet.cli status
python -m hearthnet.cli caps --remote-only
```
---
## 4. Multi-Device / Multi-Browser
### Two browsers on the same LAN
1. Start HearthNet on one machine with `host = "0.0.0.0"` in `config.toml`
2. Open `http://<machine-ip>:7860` in any browser on the LAN
Both browsers connect to the **same node** β they share the same bus, peer list, and capabilities.
### Two separate nodes (two machines)
1. Machine A: `python -m hearthnet.cli run`
2. Machine B: `python -m hearthnet.cli run`
3. Both must be on the same LAN (mDNS) or share a relay URL
Once discovered, Machine B's bus sees Machine A's capabilities (e.g. `llm.chat@1.0`).
Calls made from Machine B's UI automatically route to whichever node has the best-scoring provider.
### Testing two clients in one browser (different tabs / incognito)
Each browser tab that opens the Gradio UI is just a view onto the same node.
To simulate two truly independent clients, run two nodes on different ports:
```bash
# Terminal 1
HEARTHNET_TRANSPORT_PORT=7081 HEARTHNET_UI_PORT=7861 python -m hearthnet.cli run
# Terminal 2
HEARTHNET_TRANSPORT_PORT=7082 HEARTHNET_UI_PORT=7862 python -m hearthnet.cli run
```
Open `http://127.0.0.1:7861` and `http://127.0.0.1:7862` in two browser tabs.
Both nodes discover each other via mDNS within a few seconds.
### Playwright E2E test for two nodes
```python
# tests/test_e2e_playwright.py already includes:
# - TestUiLoads β all 6 tabs present
# - TestAskTab β real LLM/fallback response
# - TestResponsiveLayout β mobile viewport
```
Run:
```bash
python -m pytest tests/test_e2e_playwright.py -v
```
---
## 5. RAG β Adding to the Knowledge Base
### Via the UI (Settings tab β RAG β Ingest Documents)
1. Open the Settings tab
2. Expand "RAG β Ingest Documents"
3. Enter a corpus name (default: `community`)
4. Upload a `.txt`, `.md`, or `.pdf` file
5. Click **Ingest**
The document is chunked (1000 tokens, 200-token overlap), embedded, and stored in ChromaDB.
### Via CLI
```bash
python -m hearthnet.cli rag ingest ./docs/emergency-procedures.md --corpus community
python -m hearthnet.cli rag ingest ./manuals/first-aid.pdf --corpus medical
# List corpora
python -m hearthnet.cli rag list
```
### Via the bus (programmatic)
```python
result = await bus.call(
"rag.ingest", (1, 0),
{"input": {
"corpus": "community",
"doc_title": "Emergency procedures",
"text": "... full document text ...",
}}
)
```
### Using RAG in the Ask tab
Select a corpus from the dropdown in the Ask tab. HearthNet retrieves
the top-k most relevant chunks and provides them as context to the LLM.
---
## 6. LLM Backends
HearthNet tries backends in this order:
| Priority | Backend | When to use |
|----------|---------|-------------|
| 1 | **Ollama** | Best UX. Zero-config. `ollama serve` + `ollama pull <model>` |
| 2 | **llama.cpp HTTP** | Direct GPU control. Start with `./server -m model.gguf` |
| 3 | **OpenBMB / MiniCPM** | Small local models (4β8B). Pi-friendly |
| 4 | **Nemotron** | NVIDIA cloud or NIM server |
| 5 | **Generic OpenAI-compat** | LM Studio, vLLM, any OpenAI-compatible server |
| 6 | **HF Transformers** | Last resort local inference |
Cloud APIs (OpenAI, Nemotron cloud) are **never the default** β they require explicit config and are automatically deregistered when the node goes offline.
### Ollama
```bash
# Install: https://ollama.com
ollama pull llama3.2:3b # 2 GB β works on 4 GB RAM
ollama pull qwen2.5:7b # 5 GB β good quality
ollama pull minicpm3:4b # 3 GB β Pi-friendly
```
```toml
[[llm.backends]]
name = "ollama"
url = "http://localhost:11434"
```
### llama.cpp HTTP server (recommended)
```bash
./server -m models/qwen2.5-7b-q4_k_m.gguf --port 8080 -c 4096
```
```toml
[[llm.backends]]
name = "llama_cpp"
url = "http://localhost:8080"
model = "qwen2.5-7b"
```
### OpenBMB MiniCPM (via vLLM)
```bash
vllm serve openbmb/MiniCPM4-8B --port 8000
```
```toml
[[llm.backends]]
name = "openbmb"
url = "http://localhost:8000"
model = "openbmb/MiniCPM4-8B"
```
### Nemotron (cloud or NIM)
```bash
export NVIDIA_API_KEY=nvapi-xxx
```
```toml
[[llm.backends]]
name = "nemotron"
url = "https://integrate.api.nvidia.com/v1"
model = "nvidia/nemotron-mini-4b-instruct"
api_key_env = "NVIDIA_API_KEY"
```
---
## 7. Creating and Managing a Community
A **community** is a signed group manifest with member trust levels.
### Create a new community
```bash
python -m hearthnet.cli init --name "My Neighborhood" --profile anchor
```
This:
1. Generates Ed25519 keys in `~/.hearthnet/keys/`
2. Creates a community manifest signed by the root key
3. Writes `~/.hearthnet/config.toml`
### Join an existing community
```bash
python -m hearthnet.cli invite redeem "hnvite://v1/..."
```
### Check community status
```bash
python -m hearthnet.cli status
```
---
## 8. Inviting Other Nodes
### Generate an invite link (UI)
Settings tab β "Invite a Node" β enter trust level β click **Generate Invite Link**.
### Generate an invite link (CLI)
```bash
python -m hearthnet.cli invite create --node-id ed25519:xxx --level member
# Prints: hnvite://v1/...
```
### Redeem on the new node
```bash
python -m hearthnet.cli invite redeem "hnvite://v1/..."
```
### Mobile (M22)
The mobile app (Flutter) can scan a QR code displayed by:
```bash
python -m hearthnet.cli invite create --qr
```
Or via the Settings tab β Invite a Node β the link can be pasted into the app's
"Join Community" screen.
---
## 9. Extending HearthNet
### Adding a new capability (service)
1. Create `hearthnet/services/myservice/service.py`
```python
# Spec reference: docs/M03-bus.md Β§4 (Service Protocol)
from hearthnet.services.base import Service
from hearthnet.bus.capability import CapabilityDescriptor, RouteRequest
class MyService(Service):
name = "myservice"
version = "1.0"
def capabilities(self):
desc = CapabilityDescriptor(
name="myservice.do@1.0",
version=(1, 0),
stability="beta",
request_schema={},
response_schema=None,
stream_schema=None,
params={},
max_concurrent=4,
trust_required="member",
timeout_seconds=30,
idempotent=True,
)
return [(desc, self.handle_do, None)]
async def handle_do(self, req: RouteRequest) -> dict:
inp = req.body.get("input", {})
return {"output": {"result": f"processed: {inp}"}, "meta": {}}
async def start(self): pass
async def stop(self): pass
def health(self): return {"status": "ok"}
```
2. Register with the bus in `hearthnet/node.py`:
```python
from hearthnet.services.myservice.service import MyService
bus.register_service(MyService())
```
3. Add tests in `tests/test_myservice.py`.
### Adding a new LLM backend
Implement `LlmBackend` (Protocol in `hearthnet/services/llm/backends/base.py`):
```python
# Spec: docs/M04-llm.md Β§3.1
class MyLlmBackend:
name = "myllm"
models = [BackendModel(name="my-model", family="local", context_length=8192, requires_internet=False)]
async def chat(self, messages, *, model, stream=False, temperature=0.7, max_tokens=1024, **kw):
... # call your server, return ChatResult or AsyncIterator[Token]
async def complete(self, prompt, *, model, **kw): ...
async def warm(self): pass
async def close(self): pass
def health(self): return {"status": "ok"}
```
Then register it in `LlmService.__init__` alongside the other backends.
### Adding a new UI tab
1. Create `hearthnet/ui/tabs/mytab.py`
```python
# Spec: docs/M08-ui.md Β§5
def build_mytab(bus=None):
import gradio as gr
with gr.Column():
gr.Markdown("### My Tab")
...
```
2. Add it to `hearthnet/ui/app.py` inside the `gr.Tabs()` block:
```python
with gr.Tab("MyTab"):
from hearthnet.ui.tabs.mytab import build_mytab
build_mytab(self._bus)
```
---
## 10. Troubleshooting
### No LLM responses
1. Check Ollama is running: `ollama list`
2. Check `python -m hearthnet.cli doctor`
3. Check `python -m hearthnet.cli caps` β does `llm.chat@1.0` appear?
### Peers not discovered
1. Are both machines on the same LAN subnet?
2. Is mDNS blocked? Try enabling UDP fallback in config
3. `python -m hearthnet.cli status` β what does it show?
### RAG returns no results
1. Did you ingest documents? Settings tab β RAG β Ingest Documents
2. `python -m hearthnet.cli rag list` β are corpora listed?
3. Embedding model must be loaded β check `python -m hearthnet.cli doctor`
### Config file location
```
~/.hearthnet/config.toml (Linux/macOS)
%USERPROFILE%\.hearthnet\config.toml (Windows)
```
### Log files
```bash
python -m hearthnet.cli log --follow
# Or look at:
~/.hearthnet/logs/hearthnet.log
```
### Emergency mode stuck "offline"
```bash
# Force a connectivity check:
python -m hearthnet.cli call emergency.probe@1.0 '{}'
# Or in UI: Emergency tab β Run Connectivity Probe
```
---
## 11. How Routing Works
**Spec:** [docs/modules/M03-bus.md](../modules/M03-bus.md) Β§3.5 / Β§5.4
**Implementation:** `hearthnet/bus/router.py`, `hearthnet/bus/__init__.py`
### The capability bus
Every node has a `CapabilityBus`. Services register their capabilities on startup.
When another node or the UI calls `bus.call("llm.chat", (1, 0), body)`, the bus:
1. **Route selection** β `Router.route(req)` scores all registered providers (local and remote) using the score formula below.
2. **Local-first** β local handlers always get priority over remote ones (lower latency, no serialization).
3. **Call dispatch** β if local: `await entry.handler(req)`; if remote: `await transport.call(node_id, req)`.
4. **Health update** β `HealthTracker.record(entry, success, latency_ms)` updates rolling success-rate and latency EMA.
### Score formula
```
score = (1.0 if is_local else 0.5)
+ success_rate * 0.3
- (in_flight / max_concurrent) * 0.2
+ (1.0 if not quarantined else -999)
```
A quarantined entry (repeated failures) scores `-999` and is skipped until the cooldown expires.
### Params-based routing
When a capability descriptor is registered with **params** (e.g. `{"corpus": "medical", "requires_internet": False}`), the router also checks whether the caller's `body["params"]` are compatible:
```python
def _corpus_matches(offered: dict, requested: dict) -> bool:
return requested.get("corpus", offered["corpus"]) == offered["corpus"]
```
This lets multiple nodes serve the same capability name with different parameters, and callers select the one that matches their requirements.
### Sticky sessions (M10 Chat)
Pass `session_id` to `bus.call(...)` to pin subsequent calls to the same node:
```python
result = await bus.call("chat.history", (1, 0), body, session_id="s:abc123")
```
Subsequent calls with the same `session_id` route to the same entry (sticky routing).
### Routing a call to a specific node
Use the `InMemoryTransport` directly (in tests) or send an HTTP request to the transport server:
```
POST http://<node-host>:7080/call
Content-Type: application/json
{
"capability": "llm.chat",
"version_req": [1, 0],
"body": {"input": {"messages": [{"role": "user", "content": "Hello"}]}}
}
```
### Offline/Emergency routing
When `Detector.apply_probe_results({"internet": False})` marks the node offline:
- All capabilities with `requires_internet=True` are **deregistered** from the bus
- Calls that were routed to internet-only providers now fail over to local providers
- This is automatic β callers see no difference
---
## 12. Creating a Special-Feature Node
A **special-feature node** is any node where you register a non-default set of capabilities.
### OCR-only node (medical document reading)
```python
# hearthnet/examples/ocr_node.py
from hearthnet.node import HearthNode
from hearthnet.services.ocr.service import OcrService # M17
from hearthnet.ui.app import build_ui
node = HearthNode("ocr-node-01", "OCR Specialist", "ed25519:your-community")
node.bus.register_service(OcrService(backend="tesseract"))
# Do NOT install LLM or RAG services if this node should only do OCR
ui = build_ui(bus=node.bus, display_name=node.display_name, node_id=node.node_id, community_id=node.community_id)
ui.build().launch(server_port=7865)
```
Any other node in the community can now call `ocr.extract@1.0` and the bus
automatically routes it to this specialist node.
### Medical-RAG node (EBKH evidence base)
```python
from hearthnet.services.demo import RagService
from hearthnet.services.llm.service import LlmService
# Install LLM with a domain-specific system prompt
llm = LlmService(model="ollama:meditron-7b")
rag = RagService(corpus="medical-ebkh")
node.bus.register_service(llm)
node.bus.register_service(rag)
# Optionally seed the RAG corpus on startup
import asyncio
asyncio.run(node.rag.ingest("medical-ebkh", title="WHO First Aid", text="..."))
```
### Anchor node (high-availability, no UI)
An **anchor** node (`profile="anchor"`) is designed for always-on servers or Pis:
```bash
python -m hearthnet.cli run --profile anchor --no-ui
```
```toml
[identity]
profile = "anchor"
[transport]
host = "0.0.0.0"
port = 7080
[ui]
enabled = false # anchor nodes typically don't serve a web UI
```
Anchor nodes act as relay points (M15) and capability hubs. Other nodes discover them and offload compute tasks.
### Multilingual translation node (M18)
```python
from hearthnet.services.translation.service import TranslationService
node.bus.register_service(
TranslationService(backend="helsinki-nlp", languages=["de", "fr", "es", "ar"])
)
```
Callers use `translation.translate@1.0` with `{"input": {"text": "...", "source": "en", "target": "de"}}`.
### Civil Defense node (M31) β emergency broadcast
```python
from hearthnet.services.civil_defense.service import CivilDefenseService
node.bus.register_service(
CivilDefenseService(
broadcast_endpoints=["239.255.42.42:42425"], # UDP multicast
priority_filter="critical",
)
)
```
### Combining capabilities (full-service node)
```python
node.install_demo_services() # LLM, RAG, Marketplace, Chat
# Then add specialist services on top:
node.bus.register_service(OcrService())
node.bus.register_service(TranslationService())
node.bus.register_service(SttTtsService())
```
The bus merges all capabilities. Peer nodes discover all of them via the manifest exchange.
### Verified capabilities in the UI
The Settings tab β "Connected Peers & Capabilities" β Refresh shows the live list
of what each peer node offers. You can verify routing is correct before deploying.
|