| """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 |
| """) |
|
|