Spaces:
Running on Zero
Running on Zero
GitHub Actions commited on
Commit ·
29530f8
1
Parent(s): dfa947a
fix: corpus dropdown, LLM error display, chat UX, invite endpoint, getting started text
Browse files- ask.py: _get_corpora() scans rag.list_corpora + registry for rag.query entries
Refresh Corpora button added
LLM errors surfaced as text instead of silent 'No response'
- chat.py: show MY node_id, HF Space single-node note, broadcast with '*',
delivery status 'direct'/'queued' shown clearly, auto echo for self-msg
- getting_started.py: remove 'pip install hearthnet' (not on PyPI), link to HF Space
- settings.py: invite uses SPACE_HOST env var on HF Space (not 127.0.0.1)
- app.py: stable node_id from SPACE_HOST hash, register rag.list_corpora capability
- app.py +24 -3
- hearthnet/ui/tabs/ask.py +59 -11
- hearthnet/ui/tabs/chat.py +84 -29
- hearthnet/ui/tabs/getting_started.py +4 -7
- hearthnet/ui/tabs/settings.py +37 -18
app.py
CHANGED
|
@@ -163,6 +163,10 @@ def _build_node():
|
|
| 163 |
Uses HfLocalBackend (SmolLM2-135M) so inference works without Ollama.
|
| 164 |
Falls back to _UnavailableBackend if transformers is not installed.
|
| 165 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
from hearthnet.node import HearthNode
|
| 167 |
from hearthnet.services.chat.service import ChatService
|
| 168 |
from hearthnet.services.demo import RagService as DemoRagService
|
|
@@ -171,10 +175,16 @@ def _build_node():
|
|
| 171 |
from hearthnet.services.llm.service import LlmService
|
| 172 |
from hearthnet.services.marketplace.service import MarketplaceService
|
| 173 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
node = HearthNode(
|
| 175 |
-
node_id=
|
| 176 |
-
display_name=
|
| 177 |
-
community_id="ed25519:hf-space-
|
| 178 |
)
|
| 179 |
|
| 180 |
# LLM — HF Transformers backend (SmolLM2 by default)
|
|
@@ -253,6 +263,17 @@ def _build_node():
|
|
| 253 |
rag.documents = list(SEED_CORPUS)
|
| 254 |
node.bus.register_service(rag)
|
| 255 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
# Marketplace, Chat, Files
|
| 257 |
node.bus.register_service(MarketplaceService())
|
| 258 |
node.bus.register_service(ChatService(node.node_id))
|
|
|
|
| 163 |
Uses HfLocalBackend (SmolLM2-135M) so inference works without Ollama.
|
| 164 |
Falls back to _UnavailableBackend if transformers is not installed.
|
| 165 |
"""
|
| 166 |
+
import hashlib
|
| 167 |
+
import os
|
| 168 |
+
import socket
|
| 169 |
+
|
| 170 |
from hearthnet.node import HearthNode
|
| 171 |
from hearthnet.services.chat.service import ChatService
|
| 172 |
from hearthnet.services.demo import RagService as DemoRagService
|
|
|
|
| 175 |
from hearthnet.services.llm.service import LlmService
|
| 176 |
from hearthnet.services.marketplace.service import MarketplaceService
|
| 177 |
|
| 178 |
+
# Generate a stable node_id from the HF Space hostname (so it doesn't change on restart)
|
| 179 |
+
_host = os.getenv("SPACE_HOST", socket.gethostname())
|
| 180 |
+
_suffix = hashlib.sha256(_host.encode()).hexdigest()[:8]
|
| 181 |
+
_node_id = f"hf-space-{_suffix}"
|
| 182 |
+
_display = os.getenv("SPACE_TITLE", f"HearthNet Space ({_suffix})")
|
| 183 |
+
|
| 184 |
node = HearthNode(
|
| 185 |
+
node_id=_node_id,
|
| 186 |
+
display_name=_display,
|
| 187 |
+
community_id="ed25519:hf-space-community",
|
| 188 |
)
|
| 189 |
|
| 190 |
# LLM — HF Transformers backend (SmolLM2 by default)
|
|
|
|
| 263 |
rag.documents = list(SEED_CORPUS)
|
| 264 |
node.bus.register_service(rag)
|
| 265 |
|
| 266 |
+
# Register a synthetic rag.list_corpora so the Ask tab can discover corpora
|
| 267 |
+
from hearthnet.bus.capability import CapabilityDescriptor, RouteRequest
|
| 268 |
+
|
| 269 |
+
async def _list_corpora(req: RouteRequest) -> dict:
|
| 270 |
+
return {"output": {"corpora": ["community"]}, "meta": {}}
|
| 271 |
+
|
| 272 |
+
node.bus.register_capability(
|
| 273 |
+
CapabilityDescriptor(name="rag.list_corpora", version=(1, 0)),
|
| 274 |
+
_list_corpora,
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
# Marketplace, Chat, Files
|
| 278 |
node.bus.register_service(MarketplaceService())
|
| 279 |
node.bus.register_service(ChatService(node.node_id))
|
hearthnet/ui/tabs/ask.py
CHANGED
|
@@ -14,9 +14,40 @@ Spec: docs/M04-llm.md, docs/M05-rag.md, docs/M03-bus.md §4
|
|
| 14 |
from __future__ import annotations
|
| 15 |
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
def build_ask_tab(bus=None):
|
| 18 |
import gradio as gr
|
| 19 |
|
|
|
|
|
|
|
|
|
|
| 20 |
with gr.Column():
|
| 21 |
gr.Markdown("""### 💬 Ask the Mesh
|
| 22 |
|
|
@@ -35,16 +66,17 @@ to the best available LLM node — either on this device or on a peer.
|
|
| 35 |
with gr.Row():
|
| 36 |
corpus_selector = gr.Dropdown(
|
| 37 |
label="RAG Corpus (leave blank for direct LLM)",
|
| 38 |
-
choices=
|
| 39 |
-
value=
|
| 40 |
scale=3,
|
| 41 |
)
|
| 42 |
model_selector = gr.Dropdown(
|
| 43 |
label="Model (auto = bus picks best node)",
|
| 44 |
choices=["auto"],
|
| 45 |
value="auto",
|
| 46 |
-
scale=
|
| 47 |
)
|
|
|
|
| 48 |
|
| 49 |
chatbot = gr.Chatbot(
|
| 50 |
label="Conversation",
|
|
@@ -65,6 +97,10 @@ to the best available LLM node — either on this device or on a peer.
|
|
| 65 |
sources_out = gr.JSON(label="📚 RAG Sources", visible=False, scale=2)
|
| 66 |
route_out = gr.JSON(label="🛣️ Routing Trace", visible=False, scale=2)
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
async def handle_send(message: str, history: list, corpus: str, model: str):
|
| 69 |
if not message.strip():
|
| 70 |
return history, "", gr.update(visible=False), gr.update(visible=False)
|
|
@@ -132,14 +168,25 @@ to the best available LLM node — either on this device or on a peer.
|
|
| 132 |
(1, 0),
|
| 133 |
{"params": params, "input": {"messages": llm_messages}},
|
| 134 |
)
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
"
|
| 139 |
-
|
| 140 |
-
"
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
history.append({"role": "assistant", "content": reply})
|
| 145 |
|
|
@@ -160,6 +207,7 @@ to the best available LLM node — either on this device or on a peer.
|
|
| 160 |
gr.update(visible=True, value=trace),
|
| 161 |
)
|
| 162 |
|
|
|
|
| 163 |
send_btn.click(
|
| 164 |
handle_send,
|
| 165 |
inputs=[msg_input, chatbot, corpus_selector, model_selector],
|
|
|
|
| 14 |
from __future__ import annotations
|
| 15 |
|
| 16 |
|
| 17 |
+
def _get_corpora(bus) -> list[str]:
|
| 18 |
+
"""Scan the bus registry for all rag.query corpus names."""
|
| 19 |
+
if bus is None:
|
| 20 |
+
return []
|
| 21 |
+
corpora: list[str] = []
|
| 22 |
+
try:
|
| 23 |
+
# Try rag.list_corpora capability first (real RagService has it)
|
| 24 |
+
import asyncio
|
| 25 |
+
loop = asyncio.new_event_loop()
|
| 26 |
+
r = loop.run_until_complete(bus.call("rag.list_corpora", (1, 0), {"input": {}}))
|
| 27 |
+
loop.close()
|
| 28 |
+
corpora = r.get("output", {}).get("corpora", [])
|
| 29 |
+
except Exception:
|
| 30 |
+
pass
|
| 31 |
+
if not corpora:
|
| 32 |
+
# Fallback: inspect registry for rag.query entries and extract corpus param
|
| 33 |
+
try:
|
| 34 |
+
all_entries = list(bus.registry.all())
|
| 35 |
+
for entry in all_entries:
|
| 36 |
+
if entry.descriptor.name == "rag.query":
|
| 37 |
+
corpus = (entry.descriptor.params or {}).get("corpus")
|
| 38 |
+
if corpus and corpus not in corpora:
|
| 39 |
+
corpora.append(corpus)
|
| 40 |
+
except Exception:
|
| 41 |
+
pass
|
| 42 |
+
return corpora
|
| 43 |
+
|
| 44 |
+
|
| 45 |
def build_ask_tab(bus=None):
|
| 46 |
import gradio as gr
|
| 47 |
|
| 48 |
+
corpora = _get_corpora(bus)
|
| 49 |
+
corpus_choices = ["(none)"] + corpora
|
| 50 |
+
|
| 51 |
with gr.Column():
|
| 52 |
gr.Markdown("""### 💬 Ask the Mesh
|
| 53 |
|
|
|
|
| 66 |
with gr.Row():
|
| 67 |
corpus_selector = gr.Dropdown(
|
| 68 |
label="RAG Corpus (leave blank for direct LLM)",
|
| 69 |
+
choices=corpus_choices,
|
| 70 |
+
value=corpus_choices[0],
|
| 71 |
scale=3,
|
| 72 |
)
|
| 73 |
model_selector = gr.Dropdown(
|
| 74 |
label="Model (auto = bus picks best node)",
|
| 75 |
choices=["auto"],
|
| 76 |
value="auto",
|
| 77 |
+
scale=2,
|
| 78 |
)
|
| 79 |
+
refresh_corpora_btn = gr.Button("🔄 Refresh Corpora", size="sm", scale=1)
|
| 80 |
|
| 81 |
chatbot = gr.Chatbot(
|
| 82 |
label="Conversation",
|
|
|
|
| 97 |
sources_out = gr.JSON(label="📚 RAG Sources", visible=False, scale=2)
|
| 98 |
route_out = gr.JSON(label="🛣️ Routing Trace", visible=False, scale=2)
|
| 99 |
|
| 100 |
+
def refresh_corpora():
|
| 101 |
+
choices = ["(none)"] + _get_corpora(bus)
|
| 102 |
+
return gr.update(choices=choices, value=choices[0])
|
| 103 |
+
|
| 104 |
async def handle_send(message: str, history: list, corpus: str, model: str):
|
| 105 |
if not message.strip():
|
| 106 |
return history, "", gr.update(visible=False), gr.update(visible=False)
|
|
|
|
| 168 |
(1, 0),
|
| 169 |
{"params": params, "input": {"messages": llm_messages}},
|
| 170 |
)
|
| 171 |
+
|
| 172 |
+
# Surface errors clearly instead of showing "No response"
|
| 173 |
+
if "error" in result:
|
| 174 |
+
err_msg = result.get("message", result.get("error", "unknown error"))
|
| 175 |
+
reply = f"⚠️ LLM error: {err_msg}"
|
| 176 |
+
trace["llm"] = {"error": err_msg}
|
| 177 |
+
else:
|
| 178 |
+
reply = (
|
| 179 |
+
result.get("output", {}).get("message", {}).get("content")
|
| 180 |
+
or result.get("output", {}).get("text")
|
| 181 |
+
or "(empty response — model may still be loading)"
|
| 182 |
+
)
|
| 183 |
+
routed_via_llm = result.get("_routed_via", "local")
|
| 184 |
+
trace["llm"] = {
|
| 185 |
+
"capability": "llm.chat",
|
| 186 |
+
"model_requested": model if model != "auto" else "(any)",
|
| 187 |
+
"routed_via": routed_via_llm,
|
| 188 |
+
}
|
| 189 |
+
trace["routed_to"] = routed_via_llm
|
| 190 |
|
| 191 |
history.append({"role": "assistant", "content": reply})
|
| 192 |
|
|
|
|
| 207 |
gr.update(visible=True, value=trace),
|
| 208 |
)
|
| 209 |
|
| 210 |
+
refresh_corpora_btn.click(refresh_corpora, outputs=corpus_selector)
|
| 211 |
send_btn.click(
|
| 212 |
handle_send,
|
| 213 |
inputs=[msg_input, chatbot, corpus_selector, model_selector],
|
hearthnet/ui/tabs/chat.py
CHANGED
|
@@ -6,71 +6,121 @@ from __future__ import annotations
|
|
| 6 |
def build_chat_tab(bus=None):
|
| 7 |
import gradio as gr
|
| 8 |
|
|
|
|
|
|
|
| 9 |
with gr.Column():
|
| 10 |
gr.Markdown("""### 💬 Direct Messages
|
| 11 |
|
| 12 |
Send and receive messages between HearthNet nodes (M10).
|
| 13 |
Messages are **event-sourced** with Lamport clocks — delivery order is deterministic
|
| 14 |
even when nodes reconnect after an offline period.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
|
|
|
| 16 |
**How to use:**
|
| 17 |
-
1. Enter
|
| 18 |
-
2. Click **Load History** to see past messages
|
| 19 |
3. Type a message and press **Send**
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
| 22 |
- `queued` — stored locally, will deliver when recipient reconnects
|
| 23 |
-
- `delivered` — recipient acknowledged receipt
|
|
|
|
|
|
|
|
|
|
| 24 |
""")
|
| 25 |
|
| 26 |
with gr.Row():
|
| 27 |
-
peer_id = gr.Textbox(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
history_btn = gr.Button("Load History", scale=1)
|
| 29 |
|
| 30 |
-
chat_out = gr.Chatbot(label="Messages", height=
|
| 31 |
|
| 32 |
with gr.Row():
|
| 33 |
-
msg_input = gr.Textbox(label="Message", scale=7)
|
| 34 |
send_btn = gr.Button("Send", scale=1, variant="primary")
|
| 35 |
-
|
|
|
|
| 36 |
|
| 37 |
async def load_history(peer):
|
| 38 |
if bus is None:
|
| 39 |
-
return [{"role": "assistant", "content": "Bus not connected"}]
|
|
|
|
| 40 |
try:
|
| 41 |
-
r = await bus.call("chat.history", (1, 0), {"input": {"peer":
|
| 42 |
msgs = r.get("output", {}).get("messages", [])
|
|
|
|
|
|
|
| 43 |
result = []
|
|
|
|
| 44 |
for m in msgs:
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
| 48 |
return result
|
| 49 |
except Exception as e:
|
| 50 |
-
return [{"role": "assistant", "content": f"Error: {e}"}]
|
| 51 |
|
| 52 |
async def send_msg(peer, msg, history):
|
| 53 |
-
if not
|
| 54 |
return history, "", gr.update(visible=False)
|
| 55 |
history = history or []
|
| 56 |
if bus is None:
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
"chat.send",
|
| 65 |
-
(1, 0),
|
| 66 |
-
{"input": {"recipient": peer, "body": msg}},
|
| 67 |
)
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
history = history + [
|
| 70 |
{"role": "user", "content": msg},
|
| 71 |
-
{"role": "assistant", "content": f"✓ delivered={status}"},
|
| 72 |
]
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
except Exception as e:
|
| 75 |
history = history + [
|
| 76 |
{"role": "user", "content": msg},
|
|
@@ -82,5 +132,10 @@ The delivery confirmation shows whether the message was:
|
|
| 82 |
send_btn.click(
|
| 83 |
send_msg,
|
| 84 |
inputs=[peer_id, msg_input, chat_out],
|
| 85 |
-
outputs=[chat_out, msg_input,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
)
|
|
|
|
| 6 |
def build_chat_tab(bus=None):
|
| 7 |
import gradio as gr
|
| 8 |
|
| 9 |
+
my_node_id = getattr(bus, "node_id_full", None) if bus else None
|
| 10 |
+
|
| 11 |
with gr.Column():
|
| 12 |
gr.Markdown("""### 💬 Direct Messages
|
| 13 |
|
| 14 |
Send and receive messages between HearthNet nodes (M10).
|
| 15 |
Messages are **event-sourced** with Lamport clocks — delivery order is deterministic
|
| 16 |
even when nodes reconnect after an offline period.
|
| 17 |
+
""")
|
| 18 |
+
|
| 19 |
+
if my_node_id:
|
| 20 |
+
gr.Markdown(
|
| 21 |
+
f"**Your Node ID** (share this so others can message you):\n\n"
|
| 22 |
+
f"```\n{my_node_id}\n```"
|
| 23 |
+
)
|
| 24 |
|
| 25 |
+
gr.Markdown("""
|
| 26 |
**How to use:**
|
| 27 |
+
1. Enter a **Recipient Node ID** — copy it from their Settings tab
|
| 28 |
+
2. Click **Load History** to see past messages
|
| 29 |
3. Type a message and press **Send**
|
| 30 |
|
| 31 |
+
**Send to all peers:** use `*` as the recipient to broadcast to every known peer.
|
| 32 |
+
|
| 33 |
+
**Delivery status:**
|
| 34 |
+
- `direct` — sent to yourself (same node)
|
| 35 |
- `queued` — stored locally, will deliver when recipient reconnects
|
| 36 |
+
- `delivered` — recipient on a live peer node acknowledged receipt
|
| 37 |
+
|
| 38 |
+
> On the **HF Space** (single node): only self-messages (`direct`) work.
|
| 39 |
+
> For real peer messaging, run two local nodes — see Getting Started.
|
| 40 |
""")
|
| 41 |
|
| 42 |
with gr.Row():
|
| 43 |
+
peer_id = gr.Textbox(
|
| 44 |
+
label="Recipient Node ID",
|
| 45 |
+
placeholder=f"e.g. {my_node_id or 'ed25519:...'} (use * for broadcast)",
|
| 46 |
+
scale=4,
|
| 47 |
+
)
|
| 48 |
history_btn = gr.Button("Load History", scale=1)
|
| 49 |
|
| 50 |
+
chat_out = gr.Chatbot(label="Messages", height=340)
|
| 51 |
|
| 52 |
with gr.Row():
|
| 53 |
+
msg_input = gr.Textbox(label="Message", placeholder="Type a message…", scale=7)
|
| 54 |
send_btn = gr.Button("Send", scale=1, variant="primary")
|
| 55 |
+
|
| 56 |
+
status_out = gr.Markdown(visible=False)
|
| 57 |
|
| 58 |
async def load_history(peer):
|
| 59 |
if bus is None:
|
| 60 |
+
return [{"role": "assistant", "content": "⚠️ Bus not connected"}]
|
| 61 |
+
target = peer.strip() if peer else None
|
| 62 |
try:
|
| 63 |
+
r = await bus.call("chat.history", (1, 0), {"input": {"peer": target}})
|
| 64 |
msgs = r.get("output", {}).get("messages", [])
|
| 65 |
+
if not msgs:
|
| 66 |
+
return [{"role": "assistant", "content": "(no messages yet)"}]
|
| 67 |
result = []
|
| 68 |
+
node_me = getattr(bus, "node_id_full", "me")
|
| 69 |
for m in msgs:
|
| 70 |
+
sender = m.get("from", "?")
|
| 71 |
+
is_mine = sender == node_me
|
| 72 |
+
result.append({
|
| 73 |
+
"role": "user" if is_mine else "assistant",
|
| 74 |
+
"content": f"{'You' if is_mine else sender}: {m.get('body', '')}",
|
| 75 |
+
})
|
| 76 |
return result
|
| 77 |
except Exception as e:
|
| 78 |
+
return [{"role": "assistant", "content": f"Error loading history: {e}"}]
|
| 79 |
|
| 80 |
async def send_msg(peer, msg, history):
|
| 81 |
+
if not msg.strip():
|
| 82 |
return history, "", gr.update(visible=False)
|
| 83 |
history = history or []
|
| 84 |
if bus is None:
|
| 85 |
+
return (
|
| 86 |
+
history + [
|
| 87 |
+
{"role": "user", "content": msg},
|
| 88 |
+
{"role": "assistant", "content": "⚠️ Bus not connected"},
|
| 89 |
+
],
|
| 90 |
+
"",
|
| 91 |
+
gr.update(visible=False),
|
|
|
|
|
|
|
|
|
|
| 92 |
)
|
| 93 |
+
|
| 94 |
+
# Broadcast to all peers if * used
|
| 95 |
+
recipient = peer.strip() if peer else getattr(bus, "node_id_full", "")
|
| 96 |
+
if recipient == "*":
|
| 97 |
+
# Send to all known peers
|
| 98 |
+
peers_snapshot = getattr(bus, "topology_snapshot", lambda: None)()
|
| 99 |
+
all_peers = [p.get("node_id") for p in (getattr(peers_snapshot, "peers", []) or [])]
|
| 100 |
+
if not all_peers:
|
| 101 |
+
all_peers = [getattr(bus, "node_id_full", recipient)]
|
| 102 |
+
results = []
|
| 103 |
+
for p in all_peers:
|
| 104 |
+
try:
|
| 105 |
+
r = await bus.call("chat.send", (1, 0), {"input": {"recipient": p, "body": msg}})
|
| 106 |
+
results.append(r.get("output", {}).get("delivered", "queued"))
|
| 107 |
+
except Exception:
|
| 108 |
+
results.append("error")
|
| 109 |
+
history = history + [{"role": "user", "content": f"[broadcast to {len(all_peers)} peers] {msg}"}]
|
| 110 |
+
note = f"✓ Broadcast sent to {len(all_peers)} peer(s): {results}"
|
| 111 |
+
return history, "", gr.update(visible=True, value=note)
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
r = await bus.call("chat.send", (1, 0), {"input": {"recipient": recipient, "body": msg}})
|
| 115 |
+
status = r.get("output", {}).get("delivered", "queued")
|
| 116 |
history = history + [
|
| 117 |
{"role": "user", "content": msg},
|
|
|
|
| 118 |
]
|
| 119 |
+
if status == "direct":
|
| 120 |
+
# Self-message — also show it as received
|
| 121 |
+
history.append({"role": "assistant", "content": f"[echo] {msg}"})
|
| 122 |
+
note = f"✓ {status} → `{recipient}`"
|
| 123 |
+
return history, "", gr.update(visible=True, value=note)
|
| 124 |
except Exception as e:
|
| 125 |
history = history + [
|
| 126 |
{"role": "user", "content": msg},
|
|
|
|
| 132 |
send_btn.click(
|
| 133 |
send_msg,
|
| 134 |
inputs=[peer_id, msg_input, chat_out],
|
| 135 |
+
outputs=[chat_out, msg_input, status_out],
|
| 136 |
+
)
|
| 137 |
+
msg_input.submit(
|
| 138 |
+
send_msg,
|
| 139 |
+
inputs=[peer_id, msg_input, chat_out],
|
| 140 |
+
outputs=[chat_out, msg_input, status_out],
|
| 141 |
)
|
hearthnet/ui/tabs/getting_started.py
CHANGED
|
@@ -18,23 +18,20 @@ capabilities, files, and community posts — no central server required.
|
|
| 18 |
## Quick Start (any device with Python)
|
| 19 |
|
| 20 |
```bash
|
| 21 |
-
# 1.
|
| 22 |
git clone https://huggingface.co/spaces/build-small-hackathon/HearthNet
|
| 23 |
cd HearthNet
|
| 24 |
pip install -e .
|
| 25 |
|
| 26 |
-
# 2. Run
|
| 27 |
python -m hearthnet.cli run
|
| 28 |
|
| 29 |
# 3. Open the UI
|
| 30 |
# http://localhost:7860
|
| 31 |
```
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
> **PyPI package**: `pip install hearthnet` will work once the package is published.
|
| 37 |
-
> Until then use `pip install -e .` from the cloned repo.
|
| 38 |
|
| 39 |
---
|
| 40 |
|
|
|
|
| 18 |
## Quick Start (any device with Python)
|
| 19 |
|
| 20 |
```bash
|
| 21 |
+
# 1. Clone the repo (PyPI package coming soon — use git clone for now)
|
| 22 |
git clone https://huggingface.co/spaces/build-small-hackathon/HearthNet
|
| 23 |
cd HearthNet
|
| 24 |
pip install -e .
|
| 25 |
|
| 26 |
+
# 2. Run your local node
|
| 27 |
python -m hearthnet.cli run
|
| 28 |
|
| 29 |
# 3. Open the UI
|
| 30 |
# http://localhost:7860
|
| 31 |
```
|
| 32 |
|
| 33 |
+
The **HF Space** above is the public demo — single node, SmolLM2-135M, no real peer mesh.
|
| 34 |
+
A **local install** gives you Ollama/llama.cpp models, real peer discovery, file sharing, and chat.
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
---
|
| 37 |
|
hearthnet/ui/tabs/settings.py
CHANGED
|
@@ -158,37 +158,56 @@ Set `relay_url` in `~/.hearthnet/config.toml` for cross-internet connections.
|
|
| 158 |
if bus is None:
|
| 159 |
return "<p style='color:#f44'>Bus not connected — run as a real node.</p>", ""
|
| 160 |
try:
|
|
|
|
| 161 |
from pathlib import Path
|
| 162 |
|
| 163 |
from hearthnet.identity.keys import load_or_generate
|
| 164 |
from hearthnet.ui.onboarding import encode_invite, make_invite
|
| 165 |
|
| 166 |
kp = load_or_generate(Path.home() / ".hearthnet" / "keys")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
cm_prov = getattr(bus, "community_manifest_provider", None)
|
| 168 |
cm = cm_prov() if cm_prov else None
|
| 169 |
if cm is None:
|
| 170 |
-
port_obj = getattr(config, "transport", None)
|
| 171 |
-
port_val = getattr(port_obj, "port", 7080) if port_obj else 7080
|
| 172 |
link = (
|
| 173 |
f"hnvite://v1/{bus.node_id_full}"
|
| 174 |
-
f"?host=
|
| 175 |
)
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
except Exception as exc:
|
| 191 |
-
return f"<p style='color:#f44'>Error: {exc}</p>", ""
|
| 192 |
|
| 193 |
make_invite_btn.click(
|
| 194 |
gen_invite,
|
|
|
|
| 158 |
if bus is None:
|
| 159 |
return "<p style='color:#f44'>Bus not connected — run as a real node.</p>", ""
|
| 160 |
try:
|
| 161 |
+
import os
|
| 162 |
from pathlib import Path
|
| 163 |
|
| 164 |
from hearthnet.identity.keys import load_or_generate
|
| 165 |
from hearthnet.ui.onboarding import encode_invite, make_invite
|
| 166 |
|
| 167 |
kp = load_or_generate(Path.home() / ".hearthnet" / "keys")
|
| 168 |
+
# Detect whether we're on HF Space or local
|
| 169 |
+
hf_space_host = os.getenv("SPACE_HOST") # e.g. build-small-hackathon-hearthnet.hf.space
|
| 170 |
+
if hf_space_host:
|
| 171 |
+
public_host = hf_space_host
|
| 172 |
+
public_port = 443
|
| 173 |
+
transport = "https"
|
| 174 |
+
else:
|
| 175 |
+
port_obj = getattr(config, "transport", None)
|
| 176 |
+
public_port = getattr(port_obj, "port", 7080) if port_obj else 7080
|
| 177 |
+
public_host = "127.0.0.1"
|
| 178 |
+
transport = "http"
|
| 179 |
+
|
| 180 |
cm_prov = getattr(bus, "community_manifest_provider", None)
|
| 181 |
cm = cm_prov() if cm_prov else None
|
| 182 |
if cm is None:
|
|
|
|
|
|
|
| 183 |
link = (
|
| 184 |
f"hnvite://v1/{bus.node_id_full}"
|
| 185 |
+
f"?host={public_host}&port={public_port}&transport={transport}&level={level}"
|
| 186 |
)
|
| 187 |
+
qr_data = link
|
| 188 |
+
else:
|
| 189 |
+
from hearthnet.identity.manifest import Endpoint
|
| 190 |
+
|
| 191 |
+
blob = make_invite(
|
| 192 |
+
invitee_node_id_full=invitee or "ed25519:any",
|
| 193 |
+
inviter_kp=kp,
|
| 194 |
+
community_manifest=cm,
|
| 195 |
+
bootstrap_endpoints=[
|
| 196 |
+
Endpoint(transport=transport, host=public_host, port=public_port)
|
| 197 |
+
],
|
| 198 |
+
initial_level=level,
|
| 199 |
+
)
|
| 200 |
+
link = encode_invite(blob)
|
| 201 |
+
qr_data = link
|
| 202 |
+
|
| 203 |
+
note = ""
|
| 204 |
+
if hf_space_host:
|
| 205 |
+
note = f"\n\n> ℹ️ This invite uses the **HF Space URL** (`{public_host}`). Peers outside the Space can use it."
|
| 206 |
+
else:
|
| 207 |
+
note = f"\n\n> ℹ️ Host is `{public_host}:{public_port}`. Make sure this is reachable by the invitee."
|
| 208 |
+
return _qr_svg(qr_data), link + note
|
| 209 |
except Exception as exc:
|
| 210 |
+
return f"<p style='color:#f44'>Error: {exc}</p>", f"Error: {exc}"
|
| 211 |
|
| 212 |
make_invite_btn.click(
|
| 213 |
gen_invite,
|