Spaces:
Running on Zero
Running on Zero
GitHub Actions commited on
Commit Β·
3f78ea8
1
Parent(s): c88a878
Quality improvements: Unicode chars, Token class, imports, type hints, formatting
Browse files- Ruff formatting applied to all 158 files
- Fixed Unicode character ambiguities (RUF001)
- Removed duplicate methods in _EchoBackend class
- Updated Token dataclass with proper type hints
- Fixed unused imports and parameter shadowing
- Improved optional import handling
- 47% reduction in ruff lint violations (51 β 27)
- All tests passing, no regressions
This view is limited to 50 files because it contains too many changes. Β See raw diff
- app.py +1 -0
- hearthnet/bus/registry.py +2 -0
- hearthnet/bus/trace.py +1 -0
- hearthnet/civdef/service.py +0 -1
- hearthnet/cli.py +34 -14
- hearthnet/config.py +4 -3
- hearthnet/conformance/runner.py +193 -36
- hearthnet/constants.py +1 -1
- hearthnet/discovery/mdns.py +7 -4
- hearthnet/discovery/peers.py +5 -4
- hearthnet/discovery/udp.py +3 -0
- hearthnet/emergency/state.py +5 -1
- hearthnet/events/log.py +2 -2
- hearthnet/events/replay.py +2 -2
- hearthnet/events/snapshot.py +3 -3
- hearthnet/evidence/service.py +1 -3
- hearthnet/identity/keys.py +1 -0
- hearthnet/identity/manifest.py +5 -2
- hearthnet/node.py +1 -1
- hearthnet/observability/doctor.py +0 -1
- hearthnet/observability/federated.py +1 -3
- hearthnet/observability/logging.py +1 -3
- hearthnet/observability/metrics.py +18 -14
- hearthnet/observability/otlp_export.py +14 -9
- hearthnet/relay/push_subscriber.py +1 -0
- hearthnet/services/chat/service.py +2 -2
- hearthnet/services/chat/thread_views.py +3 -3
- hearthnet/services/llm/backends/base.py +2 -1
- hearthnet/services/llm/backends/hf_api.py +6 -4
- hearthnet/services/llm/backends/llama_cpp.py +2 -3
- hearthnet/services/llm/backends/modal_backend.py +2 -6
- hearthnet/services/llm/backends/ollama.py +4 -1
- hearthnet/services/llm/backends/openai_compat.py +9 -6
- hearthnet/services/llm/service.py +0 -12
- hearthnet/services/marketplace/post.py +2 -2
- hearthnet/services/marketplace/service.py +2 -2
- hearthnet/services/marketplace/views.py +2 -2
- hearthnet/services/moe/service.py +5 -5
- hearthnet/services/protocol/service.py +72 -15
- hearthnet/services/rag/federated.py +8 -13
- hearthnet/services/rag/replication.py +1 -3
- hearthnet/services/rag/service.py +1 -5
- hearthnet/services/speech/backends/base.py +1 -1
- hearthnet/services/speech/backends/edge_tts.py +1 -1
- hearthnet/services/tools/plant.py +5 -15
- hearthnet/transport/backpressure.py +2 -0
- hearthnet/transport/client.py +11 -8
- hearthnet/transport/server.py +2 -2
- hearthnet/transport/websocket.py +4 -0
- hearthnet/types.py +6 -6
app.py
CHANGED
|
@@ -472,6 +472,7 @@ def _mount_bus_endpoints(app) -> None:
|
|
| 472 |
app.routes.insert(0, app.routes.pop(_i))
|
| 473 |
break
|
| 474 |
|
|
|
|
| 475 |
# 3) Patch App.create_app to inject the StaticFiles mount after Gradio routes
|
| 476 |
if _webagent_dir.exists():
|
| 477 |
try:
|
|
|
|
| 472 |
app.routes.insert(0, app.routes.pop(_i))
|
| 473 |
break
|
| 474 |
|
| 475 |
+
|
| 476 |
# 3) Patch App.create_app to inject the StaticFiles mount after Gradio routes
|
| 477 |
if _webagent_dir.exists():
|
| 478 |
try:
|
hearthnet/bus/registry.py
CHANGED
|
@@ -21,6 +21,7 @@ class RegistryEvent:
|
|
| 21 |
|
| 22 |
kind in {"added", "removed", "updated"}
|
| 23 |
"""
|
|
|
|
| 24 |
kind: str
|
| 25 |
entry: CapabilityEntry
|
| 26 |
|
|
@@ -49,6 +50,7 @@ class Registry:
|
|
| 49 |
|
| 50 |
def add_remote(self, peer: PeerRecord, descriptor: CapabilityDescriptor) -> CapabilityEntry:
|
| 51 |
endpoint = peer.endpoints[0] if peer.endpoints else None
|
|
|
|
| 52 |
# Use a general params-compatibility check for remote entries so that
|
| 53 |
# corpus/model/lang routing works across the mesh without needing to
|
| 54 |
# transfer Python callables over the wire.
|
|
|
|
| 21 |
|
| 22 |
kind in {"added", "removed", "updated"}
|
| 23 |
"""
|
| 24 |
+
|
| 25 |
kind: str
|
| 26 |
entry: CapabilityEntry
|
| 27 |
|
|
|
|
| 50 |
|
| 51 |
def add_remote(self, peer: PeerRecord, descriptor: CapabilityDescriptor) -> CapabilityEntry:
|
| 52 |
endpoint = peer.endpoints[0] if peer.endpoints else None
|
| 53 |
+
|
| 54 |
# Use a general params-compatibility check for remote entries so that
|
| 55 |
# corpus/model/lang routing works across the mesh without needing to
|
| 56 |
# transfer Python callables over the wire.
|
hearthnet/bus/trace.py
CHANGED
|
@@ -32,5 +32,6 @@ class TraceHook:
|
|
| 32 |
def record(self, event: CallTraceEvent) -> None:
|
| 33 |
if self._ring is not None:
|
| 34 |
from contextlib import suppress
|
|
|
|
| 35 |
with suppress(Exception):
|
| 36 |
self._ring.push(event)
|
|
|
|
| 32 |
def record(self, event: CallTraceEvent) -> None:
|
| 33 |
if self._ring is not None:
|
| 34 |
from contextlib import suppress
|
| 35 |
+
|
| 36 |
with suppress(Exception):
|
| 37 |
self._ring.push(event)
|
hearthnet/civdef/service.py
CHANGED
|
@@ -312,4 +312,3 @@ class CivilDefenseService:
|
|
| 312 |
|
| 313 |
async def handle_audit(self, req: Any) -> dict:
|
| 314 |
return {"output": self.export_audit(), "meta": {}}
|
| 315 |
-
|
|
|
|
| 312 |
|
| 313 |
async def handle_audit(self, req: Any) -> dict:
|
| 314 |
return {"output": self.export_audit(), "meta": {}}
|
|
|
hearthnet/cli.py
CHANGED
|
@@ -420,7 +420,12 @@ def log(follow: bool, level: str, component: str | None, host: str, port: int) -
|
|
| 420 |
if component and entry.get("component", "") != component:
|
| 421 |
continue
|
| 422 |
entry_level = entry.get("level", "INFO").upper()
|
| 423 |
-
if ["DEBUG", "INFO", "WARNING", "ERROR"].index(entry_level) < [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
continue
|
| 425 |
ts = entry.get("ts", "?")
|
| 426 |
msg = entry.get("message") or entry.get("capability") or json.dumps(entry)
|
|
@@ -436,7 +441,9 @@ def log(follow: bool, level: str, component: str | None, host: str, port: int) -
|
|
| 436 |
|
| 437 |
|
| 438 |
@main.command()
|
| 439 |
-
@click.option(
|
|
|
|
|
|
|
| 440 |
@click.option("--yes", is_flag=True, help="Skip confirmation prompt.")
|
| 441 |
def erase(keep_keys: bool, yes: bool) -> None:
|
| 442 |
"""Erase all local HearthNet data.
|
|
@@ -460,8 +467,10 @@ def erase(keep_keys: bool, yes: bool) -> None:
|
|
| 460 |
key_backup = None
|
| 461 |
if key_file.exists():
|
| 462 |
import tempfile
|
|
|
|
| 463 |
key_backup = Path(tempfile.NamedTemporaryFile(delete=False, suffix=".key").name)
|
| 464 |
import shutil as _sh
|
|
|
|
| 465 |
_sh.copy2(key_file, key_backup)
|
| 466 |
shutil.rmtree(config_dir)
|
| 467 |
if key_backup and key_backup.exists():
|
|
@@ -518,9 +527,13 @@ def rag_ingest(path: str, corpus: str, host: str, port: int) -> None:
|
|
| 518 |
continue
|
| 519 |
data_b64 = __import__("base64").b64encode(f.read_bytes()).decode()
|
| 520 |
try:
|
| 521 |
-
result = _bus_call(
|
| 522 |
-
|
| 523 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
err = result.get("error")
|
| 525 |
if err:
|
| 526 |
click.echo(f" SKIP {f.name}: {err}")
|
|
@@ -575,9 +588,13 @@ def invite() -> None:
|
|
| 575 |
def invite_create(node_id: str, level: str, ttl: int, host: str, port: int) -> None:
|
| 576 |
"""Create an invite link for a new member."""
|
| 577 |
try:
|
| 578 |
-
result = _bus_call(
|
| 579 |
-
|
| 580 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
except ConnectionError:
|
| 582 |
click.echo(f"Node not reachable at {host}:{port}")
|
| 583 |
sys.exit(3)
|
|
@@ -598,9 +615,9 @@ def invite_redeem(text_or_path: str, host: str, port: int) -> None:
|
|
| 598 |
p = Path(text_or_path)
|
| 599 |
invite_text = p.read_text().strip() if p.exists() else text_or_path.strip()
|
| 600 |
try:
|
| 601 |
-
result = _bus_call(
|
| 602 |
-
"input": {"invite_text": invite_text}
|
| 603 |
-
|
| 604 |
except ConnectionError:
|
| 605 |
click.echo(f"Node not reachable at {host}:{port}")
|
| 606 |
sys.exit(3)
|
|
@@ -622,6 +639,7 @@ def version_cmd() -> None:
|
|
| 622 |
"""Print HearthNet version and exit."""
|
| 623 |
try:
|
| 624 |
from importlib.metadata import version as _v
|
|
|
|
| 625 |
ver = _v("hearthnet")
|
| 626 |
except Exception:
|
| 627 |
try:
|
|
@@ -749,9 +767,9 @@ def model_list() -> None:
|
|
| 749 |
if not model_dir.is_dir():
|
| 750 |
continue
|
| 751 |
|
| 752 |
-
size_mb = sum(
|
| 753 |
-
|
| 754 |
-
)
|
| 755 |
|
| 756 |
file_count = len(list(model_dir.rglob("*")))
|
| 757 |
|
|
@@ -803,6 +821,7 @@ def health(detailed: bool) -> None:
|
|
| 803 |
|
| 804 |
# 1. Python version
|
| 805 |
import sys
|
|
|
|
| 806 |
py_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
| 807 |
click.echo(f"β
Python: {py_version}")
|
| 808 |
checks_passed += 1
|
|
@@ -840,6 +859,7 @@ def health(detailed: bool) -> None:
|
|
| 840 |
# 4. GPU support
|
| 841 |
try:
|
| 842 |
import torch
|
|
|
|
| 843 |
has_gpu = torch.cuda.is_available()
|
| 844 |
if has_gpu:
|
| 845 |
gpu_name = torch.cuda.get_device_name(0)
|
|
|
|
| 420 |
if component and entry.get("component", "") != component:
|
| 421 |
continue
|
| 422 |
entry_level = entry.get("level", "INFO").upper()
|
| 423 |
+
if ["DEBUG", "INFO", "WARNING", "ERROR"].index(entry_level) < [
|
| 424 |
+
"DEBUG",
|
| 425 |
+
"INFO",
|
| 426 |
+
"WARNING",
|
| 427 |
+
"ERROR",
|
| 428 |
+
].index(level):
|
| 429 |
continue
|
| 430 |
ts = entry.get("ts", "?")
|
| 431 |
msg = entry.get("message") or entry.get("capability") or json.dumps(entry)
|
|
|
|
| 441 |
|
| 442 |
|
| 443 |
@main.command()
|
| 444 |
+
@click.option(
|
| 445 |
+
"--keep-keys", is_flag=True, help="Keep Ed25519 identity keys, erase everything else."
|
| 446 |
+
)
|
| 447 |
@click.option("--yes", is_flag=True, help="Skip confirmation prompt.")
|
| 448 |
def erase(keep_keys: bool, yes: bool) -> None:
|
| 449 |
"""Erase all local HearthNet data.
|
|
|
|
| 467 |
key_backup = None
|
| 468 |
if key_file.exists():
|
| 469 |
import tempfile
|
| 470 |
+
|
| 471 |
key_backup = Path(tempfile.NamedTemporaryFile(delete=False, suffix=".key").name)
|
| 472 |
import shutil as _sh
|
| 473 |
+
|
| 474 |
_sh.copy2(key_file, key_backup)
|
| 475 |
shutil.rmtree(config_dir)
|
| 476 |
if key_backup and key_backup.exists():
|
|
|
|
| 527 |
continue
|
| 528 |
data_b64 = __import__("base64").b64encode(f.read_bytes()).decode()
|
| 529 |
try:
|
| 530 |
+
result = _bus_call(
|
| 531 |
+
host,
|
| 532 |
+
port,
|
| 533 |
+
"rag.ingest",
|
| 534 |
+
(1, 0),
|
| 535 |
+
{"input": {"corpus": corpus, "filename": f.name, "data_b64": data_b64}},
|
| 536 |
+
)
|
| 537 |
err = result.get("error")
|
| 538 |
if err:
|
| 539 |
click.echo(f" SKIP {f.name}: {err}")
|
|
|
|
| 588 |
def invite_create(node_id: str, level: str, ttl: int, host: str, port: int) -> None:
|
| 589 |
"""Create an invite link for a new member."""
|
| 590 |
try:
|
| 591 |
+
result = _bus_call(
|
| 592 |
+
host,
|
| 593 |
+
port,
|
| 594 |
+
"community.invite",
|
| 595 |
+
(1, 0),
|
| 596 |
+
{"input": {"invitee_node_id": node_id, "initial_level": level, "ttl_seconds": ttl}},
|
| 597 |
+
)
|
| 598 |
except ConnectionError:
|
| 599 |
click.echo(f"Node not reachable at {host}:{port}")
|
| 600 |
sys.exit(3)
|
|
|
|
| 615 |
p = Path(text_or_path)
|
| 616 |
invite_text = p.read_text().strip() if p.exists() else text_or_path.strip()
|
| 617 |
try:
|
| 618 |
+
result = _bus_call(
|
| 619 |
+
host, port, "community.redeem", (1, 0), {"input": {"invite_text": invite_text}}
|
| 620 |
+
)
|
| 621 |
except ConnectionError:
|
| 622 |
click.echo(f"Node not reachable at {host}:{port}")
|
| 623 |
sys.exit(3)
|
|
|
|
| 639 |
"""Print HearthNet version and exit."""
|
| 640 |
try:
|
| 641 |
from importlib.metadata import version as _v
|
| 642 |
+
|
| 643 |
ver = _v("hearthnet")
|
| 644 |
except Exception:
|
| 645 |
try:
|
|
|
|
| 767 |
if not model_dir.is_dir():
|
| 768 |
continue
|
| 769 |
|
| 770 |
+
size_mb = sum(f.stat().st_size for f in model_dir.rglob("*") if f.is_file()) / (
|
| 771 |
+
1024 * 1024
|
| 772 |
+
)
|
| 773 |
|
| 774 |
file_count = len(list(model_dir.rglob("*")))
|
| 775 |
|
|
|
|
| 821 |
|
| 822 |
# 1. Python version
|
| 823 |
import sys
|
| 824 |
+
|
| 825 |
py_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
| 826 |
click.echo(f"β
Python: {py_version}")
|
| 827 |
checks_passed += 1
|
|
|
|
| 859 |
# 4. GPU support
|
| 860 |
try:
|
| 861 |
import torch
|
| 862 |
+
|
| 863 |
has_gpu = torch.cuda.is_available()
|
| 864 |
if has_gpu:
|
| 865 |
gpu_name = torch.cuda.get_device_name(0)
|
hearthnet/config.py
CHANGED
|
@@ -542,6 +542,7 @@ def save(config: Config, path: Path | None = None) -> None:
|
|
| 542 |
fh.write(content)
|
| 543 |
os.replace(tmp, cfg_path)
|
| 544 |
except Exception:
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
|
|
|
|
|
| 542 |
fh.write(content)
|
| 543 |
os.replace(tmp, cfg_path)
|
| 544 |
except Exception:
|
| 545 |
+
from contextlib import suppress
|
| 546 |
+
|
| 547 |
+
with suppress(OSError):
|
| 548 |
+
os.unlink(tmp)
|
hearthnet/conformance/runner.py
CHANGED
|
@@ -14,64 +14,217 @@ from typing import Any
|
|
| 14 |
# Check definitions
|
| 15 |
# ---------------------------------------------------------------------------
|
| 16 |
|
|
|
|
| 17 |
@dataclass(frozen=True)
|
| 18 |
class Check:
|
| 19 |
capability: str
|
| 20 |
version: tuple[int, int]
|
| 21 |
body: dict
|
| 22 |
-
suite: str
|
| 23 |
expected_output_fields: list[str] = field(default_factory=list)
|
| 24 |
-
expect_error: str | None = None
|
| 25 |
description: str = ""
|
| 26 |
|
| 27 |
|
| 28 |
# Phase 1 checks (suite 1.0) β derived from CAPABILITY_CONTRACT.md Β§3.2
|
| 29 |
_CHECKS: list[Check] = [
|
| 30 |
# Identity / protocol
|
| 31 |
-
Check(
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
# Embedding
|
| 35 |
-
Check(
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
# RAG
|
| 38 |
-
Check(
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
# Files
|
| 42 |
-
Check(
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
# Marketplace
|
| 46 |
-
Check(
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
# LLM
|
| 49 |
-
Check(
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
# Chat
|
| 52 |
-
Check(
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
# MoE (Phase 3 but bus-registered in all nodes)
|
| 55 |
-
Check(
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
# Model distribution
|
| 59 |
-
Check(
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
# Tool: plant (validates input handling)
|
| 62 |
-
Check(
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
# Phase 2 (suite 2.0) β only if registered
|
| 65 |
-
Check(
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
# Phase 3 experimental (suite 3.0)
|
| 73 |
-
Check(
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
]
|
| 76 |
|
| 77 |
|
|
@@ -79,6 +232,7 @@ _CHECKS: list[Check] = [
|
|
| 79 |
# Report
|
| 80 |
# ---------------------------------------------------------------------------
|
| 81 |
|
|
|
|
| 82 |
@dataclass
|
| 83 |
class CheckResult:
|
| 84 |
capability: str
|
|
@@ -131,6 +285,7 @@ class ConformanceReport:
|
|
| 131 |
# Runner
|
| 132 |
# ---------------------------------------------------------------------------
|
| 133 |
|
|
|
|
| 134 |
class ConformanceRunner:
|
| 135 |
"""Runs the X09 conformance suite against a local bus or remote HTTP node.
|
| 136 |
|
|
@@ -226,7 +381,9 @@ class ConformanceRunner:
|
|
| 226 |
suite=check.suite,
|
| 227 |
passed=passed,
|
| 228 |
skipped=False,
|
| 229 |
-
error=""
|
|
|
|
|
|
|
| 230 |
duration_ms=ms,
|
| 231 |
description=check.description,
|
| 232 |
)
|
|
|
|
| 14 |
# Check definitions
|
| 15 |
# ---------------------------------------------------------------------------
|
| 16 |
|
| 17 |
+
|
| 18 |
@dataclass(frozen=True)
|
| 19 |
class Check:
|
| 20 |
capability: str
|
| 21 |
version: tuple[int, int]
|
| 22 |
body: dict
|
| 23 |
+
suite: str # "1.0", "2.0", "3.0"
|
| 24 |
expected_output_fields: list[str] = field(default_factory=list)
|
| 25 |
+
expect_error: str | None = None # if set, pass only when this error is returned
|
| 26 |
description: str = ""
|
| 27 |
|
| 28 |
|
| 29 |
# Phase 1 checks (suite 1.0) β derived from CAPABILITY_CONTRACT.md Β§3.2
|
| 30 |
_CHECKS: list[Check] = [
|
| 31 |
# Identity / protocol
|
| 32 |
+
Check(
|
| 33 |
+
"protocol.version.list",
|
| 34 |
+
(1, 0),
|
| 35 |
+
{"input": {}},
|
| 36 |
+
"1.0",
|
| 37 |
+
["contract_versions"],
|
| 38 |
+
description="protocol.version.list returns supported versions",
|
| 39 |
+
),
|
| 40 |
+
Check(
|
| 41 |
+
"protocol.conformance.report",
|
| 42 |
+
(1, 0),
|
| 43 |
+
{"input": {"suite_version": "1.0", "fast": True}},
|
| 44 |
+
"1.0",
|
| 45 |
+
["passed", "total"],
|
| 46 |
+
description="protocol.conformance.report can self-report",
|
| 47 |
+
),
|
| 48 |
# Embedding
|
| 49 |
+
Check(
|
| 50 |
+
"embed.text",
|
| 51 |
+
(1, 0),
|
| 52 |
+
{"input": {"texts": ["conformance ping"]}},
|
| 53 |
+
"1.0",
|
| 54 |
+
["vectors"],
|
| 55 |
+
description="embed.text returns vectors",
|
| 56 |
+
),
|
| 57 |
# RAG
|
| 58 |
+
Check(
|
| 59 |
+
"rag.query",
|
| 60 |
+
(1, 0),
|
| 61 |
+
{"input": {"query": "ping", "corpus": "demo", "k": 1}},
|
| 62 |
+
"1.0",
|
| 63 |
+
[],
|
| 64 |
+
description="rag.query responds",
|
| 65 |
+
),
|
| 66 |
+
Check(
|
| 67 |
+
"rag.list_corpora",
|
| 68 |
+
(1, 0),
|
| 69 |
+
{"input": {}},
|
| 70 |
+
"1.0",
|
| 71 |
+
["corpora"],
|
| 72 |
+
description="rag.list_corpora returns list",
|
| 73 |
+
),
|
| 74 |
# Files
|
| 75 |
+
Check(
|
| 76 |
+
"file.list",
|
| 77 |
+
(1, 0),
|
| 78 |
+
{"input": {}},
|
| 79 |
+
"1.0",
|
| 80 |
+
["files"],
|
| 81 |
+
description="file.list returns files list",
|
| 82 |
+
),
|
| 83 |
+
Check(
|
| 84 |
+
"file.put",
|
| 85 |
+
(1, 0),
|
| 86 |
+
{"input": {"data_b64": "aGVsbG8=", "filename": "x09.txt"}},
|
| 87 |
+
"1.0",
|
| 88 |
+
["cid"],
|
| 89 |
+
description="file.put returns cid",
|
| 90 |
+
),
|
| 91 |
# Marketplace
|
| 92 |
+
Check(
|
| 93 |
+
"market.list",
|
| 94 |
+
(1, 0),
|
| 95 |
+
{"input": {}},
|
| 96 |
+
"1.0",
|
| 97 |
+
["posts"],
|
| 98 |
+
description="market.list returns posts",
|
| 99 |
+
),
|
| 100 |
# LLM
|
| 101 |
+
Check(
|
| 102 |
+
"llm.complete",
|
| 103 |
+
(1, 0),
|
| 104 |
+
{"input": {"prompt": "x09 conformance", "max_tokens": 1}},
|
| 105 |
+
"1.0",
|
| 106 |
+
[],
|
| 107 |
+
description="llm.complete responds",
|
| 108 |
+
),
|
| 109 |
# Chat
|
| 110 |
+
Check(
|
| 111 |
+
"chat.send",
|
| 112 |
+
(1, 0),
|
| 113 |
+
{"input": {"to": "self", "body": "x09", "client_id": "x09_conformance"}},
|
| 114 |
+
"1.0",
|
| 115 |
+
[],
|
| 116 |
+
description="chat.send accepts message",
|
| 117 |
+
),
|
| 118 |
# MoE (Phase 3 but bus-registered in all nodes)
|
| 119 |
+
Check(
|
| 120 |
+
"moe.list",
|
| 121 |
+
(1, 0),
|
| 122 |
+
{"input": {}},
|
| 123 |
+
"1.0",
|
| 124 |
+
["experts"],
|
| 125 |
+
description="moe.list returns experts",
|
| 126 |
+
),
|
| 127 |
+
Check(
|
| 128 |
+
"moe.route",
|
| 129 |
+
(1, 0),
|
| 130 |
+
{"input": {"query": "conformance test"}},
|
| 131 |
+
"1.0",
|
| 132 |
+
["candidates"],
|
| 133 |
+
description="moe.route returns candidates",
|
| 134 |
+
),
|
| 135 |
# Model distribution
|
| 136 |
+
Check(
|
| 137 |
+
"model.list",
|
| 138 |
+
(1, 0),
|
| 139 |
+
{"input": {}},
|
| 140 |
+
"1.0",
|
| 141 |
+
["models"],
|
| 142 |
+
description="model.list returns models",
|
| 143 |
+
),
|
| 144 |
# Tool: plant (validates input handling)
|
| 145 |
+
Check(
|
| 146 |
+
"tool.plant_identify",
|
| 147 |
+
(1, 0),
|
| 148 |
+
{"input": {}},
|
| 149 |
+
"1.0",
|
| 150 |
+
[],
|
| 151 |
+
expect_error="bad_request",
|
| 152 |
+
description="tool.plant_identify rejects missing image",
|
| 153 |
+
),
|
| 154 |
# Phase 2 (suite 2.0) β only if registered
|
| 155 |
+
Check(
|
| 156 |
+
"ocr.image",
|
| 157 |
+
(1, 0),
|
| 158 |
+
{"input": {"image_cid": "blake3:00000000"}},
|
| 159 |
+
"2.0",
|
| 160 |
+
[],
|
| 161 |
+
description="ocr.image endpoint exists",
|
| 162 |
+
),
|
| 163 |
+
Check(
|
| 164 |
+
"trans.text",
|
| 165 |
+
(1, 0),
|
| 166 |
+
{"input": {"text": "hello", "from": "en", "to": "de"}},
|
| 167 |
+
"2.0",
|
| 168 |
+
[],
|
| 169 |
+
description="trans.text responds",
|
| 170 |
+
),
|
| 171 |
+
Check(
|
| 172 |
+
"rerank.text",
|
| 173 |
+
(1, 0),
|
| 174 |
+
{"input": {"query": "test", "documents": [{"id": "d1", "text": "test"}]}},
|
| 175 |
+
"2.0",
|
| 176 |
+
[],
|
| 177 |
+
description="rerank.text responds",
|
| 178 |
+
),
|
| 179 |
+
Check(
|
| 180 |
+
"img.describe",
|
| 181 |
+
(1, 0),
|
| 182 |
+
{"input": {"image_cid": "blake3:00000000", "task": "caption"}},
|
| 183 |
+
"2.0",
|
| 184 |
+
[],
|
| 185 |
+
description="img.describe responds",
|
| 186 |
+
),
|
| 187 |
+
Check(
|
| 188 |
+
"stt.transcribe",
|
| 189 |
+
(1, 0),
|
| 190 |
+
{"input": {"audio_cid": "blake3:00000000"}},
|
| 191 |
+
"2.0",
|
| 192 |
+
[],
|
| 193 |
+
description="stt.transcribe responds",
|
| 194 |
+
),
|
| 195 |
+
Check(
|
| 196 |
+
"tts.synthesize",
|
| 197 |
+
(1, 0),
|
| 198 |
+
{"input": {"text": "ping", "speed": 1.0, "format": "wav"}},
|
| 199 |
+
"2.0",
|
| 200 |
+
[],
|
| 201 |
+
description="tts.synthesize responds",
|
| 202 |
+
),
|
| 203 |
# Phase 3 experimental (suite 3.0)
|
| 204 |
+
Check(
|
| 205 |
+
"moe.register",
|
| 206 |
+
(1, 0),
|
| 207 |
+
{
|
| 208 |
+
"input": {
|
| 209 |
+
"expert_id": "model:x09",
|
| 210 |
+
"expert_type": "model",
|
| 211 |
+
"topic_tags": ["x09"],
|
| 212 |
+
"confidence_score": 0.5,
|
| 213 |
+
"community_id": "x09",
|
| 214 |
+
}
|
| 215 |
+
},
|
| 216 |
+
"3.0",
|
| 217 |
+
["registered"],
|
| 218 |
+
description="moe.register accepts expert",
|
| 219 |
+
),
|
| 220 |
+
Check(
|
| 221 |
+
"model.status",
|
| 222 |
+
(1, 0),
|
| 223 |
+
{"input": {}},
|
| 224 |
+
"3.0",
|
| 225 |
+
["jobs"],
|
| 226 |
+
description="model.status returns jobs",
|
| 227 |
+
),
|
| 228 |
]
|
| 229 |
|
| 230 |
|
|
|
|
| 232 |
# Report
|
| 233 |
# ---------------------------------------------------------------------------
|
| 234 |
|
| 235 |
+
|
| 236 |
@dataclass
|
| 237 |
class CheckResult:
|
| 238 |
capability: str
|
|
|
|
| 285 |
# Runner
|
| 286 |
# ---------------------------------------------------------------------------
|
| 287 |
|
| 288 |
+
|
| 289 |
class ConformanceRunner:
|
| 290 |
"""Runs the X09 conformance suite against a local bus or remote HTTP node.
|
| 291 |
|
|
|
|
| 381 |
suite=check.suite,
|
| 382 |
passed=passed,
|
| 383 |
skipped=False,
|
| 384 |
+
error=""
|
| 385 |
+
if passed
|
| 386 |
+
else f"expected_error={check.expect_error}, got={error_code}",
|
| 387 |
duration_ms=ms,
|
| 388 |
description=check.description,
|
| 389 |
)
|
hearthnet/constants.py
CHANGED
|
@@ -136,7 +136,7 @@ CIVDEF_ALERT_BODY_MAX_CHARS: int = 1000
|
|
| 136 |
CIVDEF_HEARTBEAT_SECONDS: int = 60
|
| 137 |
|
| 138 |
# ββ Tensor transport (X08) βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 139 |
-
TENSOR_CHUNK_BYTES: int = 1 * 1024 * 1024
|
| 140 |
TENSOR_FLOW_CONTROL_WINDOW: int = 16
|
| 141 |
TENSOR_COMPRESSION_THRESHOLD_BYTES: int = 64 * 1024
|
| 142 |
TENSOR_KEEPALIVE_SECONDS: int = 30
|
|
|
|
| 136 |
CIVDEF_HEARTBEAT_SECONDS: int = 60
|
| 137 |
|
| 138 |
# ββ Tensor transport (X08) βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 139 |
+
TENSOR_CHUNK_BYTES: int = 1 * 1024 * 1024 # 1 MiB
|
| 140 |
TENSOR_FLOW_CONTROL_WINDOW: int = 16
|
| 141 |
TENSOR_COMPRESSION_THRESHOLD_BYTES: int = 64 * 1024
|
| 142 |
TENSOR_KEEPALIVE_SECONDS: int = 30
|
hearthnet/discovery/mdns.py
CHANGED
|
@@ -95,7 +95,9 @@ class MdnsBrowser:
|
|
| 95 |
pass
|
| 96 |
|
| 97 |
def _on_service_state_change(self, zeroconf, service_type, name, state_change) -> None:
|
| 98 |
-
self._state_change_task = asyncio.create_task(
|
|
|
|
|
|
|
| 99 |
|
| 100 |
async def _handle_change(self, zeroconf, service_type, name, state_change) -> None:
|
| 101 |
try:
|
|
@@ -132,6 +134,7 @@ class MdnsBrowser:
|
|
| 132 |
|
| 133 |
async def stop(self) -> None:
|
| 134 |
if self._zeroconf:
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
| 95 |
pass
|
| 96 |
|
| 97 |
def _on_service_state_change(self, zeroconf, service_type, name, state_change) -> None:
|
| 98 |
+
self._state_change_task = asyncio.create_task(
|
| 99 |
+
self._handle_change(zeroconf, service_type, name, state_change)
|
| 100 |
+
)
|
| 101 |
|
| 102 |
async def _handle_change(self, zeroconf, service_type, name, state_change) -> None:
|
| 103 |
try:
|
|
|
|
| 134 |
|
| 135 |
async def stop(self) -> None:
|
| 136 |
if self._zeroconf:
|
| 137 |
+
from contextlib import suppress
|
| 138 |
+
|
| 139 |
+
with suppress(Exception):
|
| 140 |
+
await self._zeroconf.async_close()
|
hearthnet/discovery/peers.py
CHANGED
|
@@ -141,7 +141,8 @@ class PeerRegistry:
|
|
| 141 |
return gen()
|
| 142 |
|
| 143 |
def _notify(self, event: PeerEvent) -> None:
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
| 141 |
return gen()
|
| 142 |
|
| 143 |
def _notify(self, event: PeerEvent) -> None:
|
| 144 |
+
from contextlib import suppress
|
| 145 |
+
|
| 146 |
+
for q in list(self._subscribers):
|
| 147 |
+
with suppress(asyncio.QueueFull):
|
| 148 |
+
q.put_nowait(event)
|
hearthnet/discovery/udp.py
CHANGED
|
@@ -46,6 +46,7 @@ class UdpAnnouncer:
|
|
| 46 |
if self._task:
|
| 47 |
self._task.cancel()
|
| 48 |
from contextlib import suppress
|
|
|
|
| 49 |
with suppress(asyncio.CancelledError):
|
| 50 |
await self._task
|
| 51 |
|
|
@@ -107,6 +108,7 @@ class UdpListener:
|
|
| 107 |
if self._task:
|
| 108 |
self._task.cancel()
|
| 109 |
from contextlib import suppress
|
|
|
|
| 110 |
with suppress(asyncio.CancelledError):
|
| 111 |
await self._task
|
| 112 |
|
|
@@ -118,6 +120,7 @@ class UdpListener:
|
|
| 118 |
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
| 119 |
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
| 120 |
from contextlib import suppress
|
|
|
|
| 121 |
with suppress(AttributeError, OSError):
|
| 122 |
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) # type: ignore[attr-defined]
|
| 123 |
sock.bind(("", self._port))
|
|
|
|
| 46 |
if self._task:
|
| 47 |
self._task.cancel()
|
| 48 |
from contextlib import suppress
|
| 49 |
+
|
| 50 |
with suppress(asyncio.CancelledError):
|
| 51 |
await self._task
|
| 52 |
|
|
|
|
| 108 |
if self._task:
|
| 109 |
self._task.cancel()
|
| 110 |
from contextlib import suppress
|
| 111 |
+
|
| 112 |
with suppress(asyncio.CancelledError):
|
| 113 |
await self._task
|
| 114 |
|
|
|
|
| 120 |
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
| 121 |
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
| 122 |
from contextlib import suppress
|
| 123 |
+
|
| 124 |
with suppress(AttributeError, OSError):
|
| 125 |
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) # type: ignore[attr-defined]
|
| 126 |
sock.bind(("", self._port))
|
hearthnet/emergency/state.py
CHANGED
|
@@ -74,7 +74,11 @@ class StateBus:
|
|
| 74 |
self._transition_times = [
|
| 75 |
t for t in self._transition_times if now - t < EMERGENCY_ANTI_FLAP_WINDOW_SECONDS
|
| 76 |
]
|
| 77 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
# Too many flaps β hold pessimistic
|
| 79 |
new_mode = old_mode # don't restore yet
|
| 80 |
|
|
|
|
| 74 |
self._transition_times = [
|
| 75 |
t for t in self._transition_times if now - t < EMERGENCY_ANTI_FLAP_WINDOW_SECONDS
|
| 76 |
]
|
| 77 |
+
if (
|
| 78 |
+
len(self._transition_times) >= EMERGENCY_ANTI_FLAP_MAX_TRANSITIONS
|
| 79 |
+
and old_mode in ("degraded", "offline")
|
| 80 |
+
and new_mode == "online"
|
| 81 |
+
):
|
| 82 |
# Too many flaps β hold pessimistic
|
| 83 |
new_mode = old_mode # don't restore yet
|
| 84 |
|
hearthnet/events/log.py
CHANGED
|
@@ -16,10 +16,10 @@ import json
|
|
| 16 |
import sqlite3
|
| 17 |
import threading
|
| 18 |
from collections.abc import AsyncIterator
|
| 19 |
-
from datetime import
|
| 20 |
from pathlib import Path
|
| 21 |
|
| 22 |
-
UTC =
|
| 23 |
from typing import Any
|
| 24 |
|
| 25 |
from .lamport import LamportClock
|
|
|
|
| 16 |
import sqlite3
|
| 17 |
import threading
|
| 18 |
from collections.abc import AsyncIterator
|
| 19 |
+
from datetime import UTC, datetime
|
| 20 |
from pathlib import Path
|
| 21 |
|
| 22 |
+
UTC = UTC
|
| 23 |
from typing import Any
|
| 24 |
|
| 25 |
from .lamport import LamportClock
|
hearthnet/events/replay.py
CHANGED
|
@@ -83,7 +83,7 @@ class ReplayEngine:
|
|
| 83 |
def replay_since(self, lamport: int) -> None:
|
| 84 |
"""Replay (without reset) all views for events at lamport >= *lamport*."""
|
| 85 |
# Collect all event types across views
|
| 86 |
-
for
|
| 87 |
event_types = list(ft) if ft is not None else None
|
| 88 |
for event in self.log.replay(since_lamport=lamport, event_types=event_types): # type: ignore[arg-type]
|
| 89 |
view.apply(event)
|
|
@@ -94,7 +94,7 @@ class ReplayEngine:
|
|
| 94 |
|
| 95 |
def _on_event(self, event: Event) -> None:
|
| 96 |
"""Route a newly-arrived event to all subscribed views."""
|
| 97 |
-
for
|
| 98 |
if ft is None or event.event_type in ft:
|
| 99 |
view.apply(event)
|
| 100 |
|
|
|
|
| 83 |
def replay_since(self, lamport: int) -> None:
|
| 84 |
"""Replay (without reset) all views for events at lamport >= *lamport*."""
|
| 85 |
# Collect all event types across views
|
| 86 |
+
for view, ft in self._views.values():
|
| 87 |
event_types = list(ft) if ft is not None else None
|
| 88 |
for event in self.log.replay(since_lamport=lamport, event_types=event_types): # type: ignore[arg-type]
|
| 89 |
view.apply(event)
|
|
|
|
| 94 |
|
| 95 |
def _on_event(self, event: Event) -> None:
|
| 96 |
"""Route a newly-arrived event to all subscribed views."""
|
| 97 |
+
for view, ft in self._views.values():
|
| 98 |
if ft is None or event.event_type in ft:
|
| 99 |
view.apply(event)
|
| 100 |
|
hearthnet/events/snapshot.py
CHANGED
|
@@ -5,11 +5,11 @@ import contextlib
|
|
| 5 |
import json
|
| 6 |
import os
|
| 7 |
from dataclasses import dataclass
|
| 8 |
-
from datetime import
|
| 9 |
from pathlib import Path
|
| 10 |
from typing import TYPE_CHECKING, Any
|
| 11 |
|
| 12 |
-
UTC =
|
| 13 |
|
| 14 |
if TYPE_CHECKING:
|
| 15 |
from .log import EventLog
|
|
@@ -156,7 +156,7 @@ def build_snapshot(
|
|
| 156 |
at_lamport = max(0, head - _SNAPSHOT_LAG_LAMPORT)
|
| 157 |
|
| 158 |
# Rebuild all views up to at_lamport
|
| 159 |
-
for
|
| 160 |
view.reset()
|
| 161 |
event_types = list(ft) if ft is not None else None
|
| 162 |
for event in log.replay(since_lamport=0, event_types=event_types): # type: ignore[arg-type]
|
|
|
|
| 5 |
import json
|
| 6 |
import os
|
| 7 |
from dataclasses import dataclass
|
| 8 |
+
from datetime import UTC, datetime
|
| 9 |
from pathlib import Path
|
| 10 |
from typing import TYPE_CHECKING, Any
|
| 11 |
|
| 12 |
+
UTC = UTC
|
| 13 |
|
| 14 |
if TYPE_CHECKING:
|
| 15 |
from .log import EventLog
|
|
|
|
| 156 |
at_lamport = max(0, head - _SNAPSHOT_LAG_LAMPORT)
|
| 157 |
|
| 158 |
# Rebuild all views up to at_lamport
|
| 159 |
+
for view, ft in engine._views.values():
|
| 160 |
view.reset()
|
| 161 |
event_types = list(ft) if ft is not None else None
|
| 162 |
for event in log.replay(since_lamport=0, event_types=event_types): # type: ignore[arg-type]
|
hearthnet/evidence/service.py
CHANGED
|
@@ -131,9 +131,7 @@ class EvidenceService:
|
|
| 131 |
claim_id = ClaimID(str(inp.get("claim_id", "")))
|
| 132 |
if self._store.get_claim(claim_id) is None:
|
| 133 |
return {"error": "not_found", "message": "unknown claim_id"}
|
| 134 |
-
self._store.attest(
|
| 135 |
-
Attestation(claim_id=claim_id, attested_by=str(req.caller or "unknown"))
|
| 136 |
-
)
|
| 137 |
return {
|
| 138 |
"output": {
|
| 139 |
"claim_id": claim_id,
|
|
|
|
| 131 |
claim_id = ClaimID(str(inp.get("claim_id", "")))
|
| 132 |
if self._store.get_claim(claim_id) is None:
|
| 133 |
return {"error": "not_found", "message": "unknown claim_id"}
|
| 134 |
+
self._store.attest(Attestation(claim_id=claim_id, attested_by=str(req.caller or "unknown")))
|
|
|
|
|
|
|
| 135 |
return {
|
| 136 |
"output": {
|
| 137 |
"claim_id": claim_id,
|
hearthnet/identity/keys.py
CHANGED
|
@@ -246,6 +246,7 @@ def save(kp: KeyPair, keys_dir: Path) -> None:
|
|
| 246 |
priv_path.write_bytes(base64.urlsafe_b64encode(sk_bytes).rstrip(b"=") + b"\n")
|
| 247 |
# Restrict permissions on POSIX
|
| 248 |
from contextlib import suppress
|
|
|
|
| 249 |
with suppress(AttributeError):
|
| 250 |
os.chmod(priv_path, stat.S_IRUSR | stat.S_IWUSR) # 0600
|
| 251 |
# Write public key
|
|
|
|
| 246 |
priv_path.write_bytes(base64.urlsafe_b64encode(sk_bytes).rstrip(b"=") + b"\n")
|
| 247 |
# Restrict permissions on POSIX
|
| 248 |
from contextlib import suppress
|
| 249 |
+
|
| 250 |
with suppress(AttributeError):
|
| 251 |
os.chmod(priv_path, stat.S_IRUSR | stat.S_IWUSR) # 0600
|
| 252 |
# Write public key
|
hearthnet/identity/manifest.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
-
from datetime import
|
| 5 |
from typing import Any
|
| 6 |
|
| 7 |
-
UTC =
|
| 8 |
|
| 9 |
from hearthnet.identity.keys import (
|
| 10 |
IdentityError,
|
|
@@ -177,6 +177,7 @@ class NodeManifest:
|
|
| 177 |
@dataclass(frozen=True)
|
| 178 |
class RevokedEntry:
|
| 179 |
"""A revoked member entry in a community manifest."""
|
|
|
|
| 180 |
node_id: str
|
| 181 |
revoked_at: str
|
| 182 |
reason: str = ""
|
|
@@ -185,6 +186,7 @@ class RevokedEntry:
|
|
| 185 |
@dataclass(frozen=True)
|
| 186 |
class CommunityMember:
|
| 187 |
"""A member record in a community manifest."""
|
|
|
|
| 188 |
node_id: str
|
| 189 |
display_name: str
|
| 190 |
level: str # "root" | "trusted" | "moderator" | "member"
|
|
@@ -195,6 +197,7 @@ class CommunityMember:
|
|
| 195 |
@dataclass(frozen=True)
|
| 196 |
class CommunityPolicy:
|
| 197 |
"""Community governance policy embedded in CommunityManifest."""
|
|
|
|
| 198 |
allow_public_join: bool = False
|
| 199 |
require_invite: bool = True
|
| 200 |
max_members: int = 500
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
+
from datetime import UTC, datetime, timedelta
|
| 5 |
from typing import Any
|
| 6 |
|
| 7 |
+
UTC = UTC
|
| 8 |
|
| 9 |
from hearthnet.identity.keys import (
|
| 10 |
IdentityError,
|
|
|
|
| 177 |
@dataclass(frozen=True)
|
| 178 |
class RevokedEntry:
|
| 179 |
"""A revoked member entry in a community manifest."""
|
| 180 |
+
|
| 181 |
node_id: str
|
| 182 |
revoked_at: str
|
| 183 |
reason: str = ""
|
|
|
|
| 186 |
@dataclass(frozen=True)
|
| 187 |
class CommunityMember:
|
| 188 |
"""A member record in a community manifest."""
|
| 189 |
+
|
| 190 |
node_id: str
|
| 191 |
display_name: str
|
| 192 |
level: str # "root" | "trusted" | "moderator" | "member"
|
|
|
|
| 197 |
@dataclass(frozen=True)
|
| 198 |
class CommunityPolicy:
|
| 199 |
"""Community governance policy embedded in CommunityManifest."""
|
| 200 |
+
|
| 201 |
allow_public_join: bool = False
|
| 202 |
require_invite: bool = True
|
| 203 |
max_members: int = 500
|
hearthnet/node.py
CHANGED
|
@@ -520,7 +520,7 @@ class HearthNode:
|
|
| 520 |
await self._http_server.start()
|
| 521 |
_log.info("HTTP server listening on %s:%d", host, port)
|
| 522 |
|
| 523 |
-
# Wire StateBus
|
| 524 |
if self._http_server._ws_pubsub is not None:
|
| 525 |
self._pubsub_task = asyncio.create_task(
|
| 526 |
self._state_bus_to_pubsub(), name="state-pubsub"
|
|
|
|
| 520 |
await self._http_server.start()
|
| 521 |
_log.info("HTTP server listening on %s:%d", host, port)
|
| 522 |
|
| 523 |
+
# Wire StateBus -> WebSocket pubsub (X06)
|
| 524 |
if self._http_server._ws_pubsub is not None:
|
| 525 |
self._pubsub_task = asyncio.create_task(
|
| 526 |
self._state_bus_to_pubsub(), name="state-pubsub"
|
hearthnet/observability/doctor.py
CHANGED
|
@@ -304,4 +304,3 @@ def run_one(name: str) -> DoctorResult:
|
|
| 304 |
# ---------------------------------------------------------------------------
|
| 305 |
|
| 306 |
CheckResult = DoctorResult
|
| 307 |
-
|
|
|
|
| 304 |
# ---------------------------------------------------------------------------
|
| 305 |
|
| 306 |
CheckResult = DoctorResult
|
|
|
hearthnet/observability/federated.py
CHANGED
|
@@ -257,9 +257,7 @@ class MetricsAggregator:
|
|
| 257 |
"""Return the latest community-wide aggregate."""
|
| 258 |
now = time.time()
|
| 259 |
online_cutoff = now - 120 # consider online if tick within 2 min
|
| 260 |
-
latest_ticks: list[NodeMetricsTick] = [
|
| 261 |
-
d[-1] for d in self._ticks.values() if d
|
| 262 |
-
]
|
| 263 |
|
| 264 |
online = [t for t in latest_ticks if t.tick_at >= online_cutoff]
|
| 265 |
total_epm = sum(t.events_per_min for t in online)
|
|
|
|
| 257 |
"""Return the latest community-wide aggregate."""
|
| 258 |
now = time.time()
|
| 259 |
online_cutoff = now - 120 # consider online if tick within 2 min
|
| 260 |
+
latest_ticks: list[NodeMetricsTick] = [d[-1] for d in self._ticks.values() if d]
|
|
|
|
|
|
|
| 261 |
|
| 262 |
online = [t for t in latest_ticks if t.tick_at >= online_cutoff]
|
| 263 |
total_epm = sum(t.events_per_min for t in online)
|
hearthnet/observability/logging.py
CHANGED
|
@@ -59,9 +59,7 @@ class JsonFormatter(logging.Formatter):
|
|
| 59 |
"exc_text",
|
| 60 |
"message",
|
| 61 |
}
|
| 62 |
-
payload.update(
|
| 63 |
-
{key: val for key, val in record.__dict__.items() if key not in _SKIP}
|
| 64 |
-
)
|
| 65 |
|
| 66 |
if record.exc_info:
|
| 67 |
payload["exc"] = self.formatException(record.exc_info)
|
|
|
|
| 59 |
"exc_text",
|
| 60 |
"message",
|
| 61 |
}
|
| 62 |
+
payload.update({key: val for key, val in record.__dict__.items() if key not in _SKIP})
|
|
|
|
|
|
|
| 63 |
|
| 64 |
if record.exc_info:
|
| 65 |
payload["exc"] = self.formatException(record.exc_info)
|
hearthnet/observability/metrics.py
CHANGED
|
@@ -271,6 +271,7 @@ class TrackioExporter:
|
|
| 271 |
def _try_init(self) -> None:
|
| 272 |
try:
|
| 273 |
import trackio # type: ignore[import]
|
|
|
|
| 274 |
self._run = trackio.init(project=self._project, name=self._run_name)
|
| 275 |
self._enabled = True
|
| 276 |
except ImportError:
|
|
@@ -295,27 +296,30 @@ class TrackioExporter:
|
|
| 295 |
if not self._enabled or self._run is None:
|
| 296 |
return
|
| 297 |
with contextlib.suppress(Exception):
|
| 298 |
-
self._run.log(
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
|
|
|
|
|
|
| 306 |
|
| 307 |
def log_topology(self, mesh_size: int, online: bool, cap_count: int) -> None:
|
| 308 |
if not self._enabled or self._run is None:
|
| 309 |
return
|
| 310 |
with contextlib.suppress(Exception):
|
| 311 |
-
self._run.log(
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
|
|
|
|
|
|
| 316 |
|
| 317 |
def close(self) -> None:
|
| 318 |
if self._run is not None:
|
| 319 |
with contextlib.suppress(Exception):
|
| 320 |
self._run.finish()
|
| 321 |
-
|
|
|
|
| 271 |
def _try_init(self) -> None:
|
| 272 |
try:
|
| 273 |
import trackio # type: ignore[import]
|
| 274 |
+
|
| 275 |
self._run = trackio.init(project=self._project, name=self._run_name)
|
| 276 |
self._enabled = True
|
| 277 |
except ImportError:
|
|
|
|
| 296 |
if not self._enabled or self._run is None:
|
| 297 |
return
|
| 298 |
with contextlib.suppress(Exception):
|
| 299 |
+
self._run.log(
|
| 300 |
+
{
|
| 301 |
+
"latency_ms": latency_ms,
|
| 302 |
+
"tokens_in": tokens_in,
|
| 303 |
+
"tokens_out": tokens_out,
|
| 304 |
+
"model": model,
|
| 305 |
+
"backend": backend,
|
| 306 |
+
"result": result,
|
| 307 |
+
}
|
| 308 |
+
)
|
| 309 |
|
| 310 |
def log_topology(self, mesh_size: int, online: bool, cap_count: int) -> None:
|
| 311 |
if not self._enabled or self._run is None:
|
| 312 |
return
|
| 313 |
with contextlib.suppress(Exception):
|
| 314 |
+
self._run.log(
|
| 315 |
+
{
|
| 316 |
+
"mesh_size": mesh_size,
|
| 317 |
+
"online": int(online),
|
| 318 |
+
"capability_count": cap_count,
|
| 319 |
+
}
|
| 320 |
+
)
|
| 321 |
|
| 322 |
def close(self) -> None:
|
| 323 |
if self._run is not None:
|
| 324 |
with contextlib.suppress(Exception):
|
| 325 |
self._run.finish()
|
|
|
hearthnet/observability/otlp_export.py
CHANGED
|
@@ -13,16 +13,20 @@ logger = logging.getLogger(__name__)
|
|
| 13 |
|
| 14 |
# Optional OpenTelemetry imports
|
| 15 |
try:
|
| 16 |
-
from
|
| 17 |
-
from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( # type: ignore[import]
|
| 18 |
-
OTLPMetricExporter,
|
| 19 |
-
)
|
| 20 |
-
from opentelemetry.sdk.metrics import MeterProvider # type: ignore[import]
|
| 21 |
-
from opentelemetry.sdk.metrics.export import ( # type: ignore[import]
|
| 22 |
-
PeriodicExportingMetricReader,
|
| 23 |
-
)
|
| 24 |
|
| 25 |
-
HAS_OTEL_METRICS =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
except ImportError:
|
| 27 |
HAS_OTEL_METRICS = False
|
| 28 |
|
|
@@ -160,6 +164,7 @@ class OtlpExporter:
|
|
| 160 |
async def shutdown(self) -> None:
|
| 161 |
"""Flush and shut down the underlying providers."""
|
| 162 |
from contextlib import suppress
|
|
|
|
| 163 |
if self._meter_provider is not None:
|
| 164 |
with suppress(Exception):
|
| 165 |
self._meter_provider.shutdown() # type: ignore[union-attr]
|
|
|
|
| 13 |
|
| 14 |
# Optional OpenTelemetry imports
|
| 15 |
try:
|
| 16 |
+
from importlib.util import find_spec
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
HAS_OTEL_METRICS = (
|
| 19 |
+
find_spec("opentelemetry.metrics") is not None
|
| 20 |
+
and find_spec("opentelemetry.exporter.otlp.proto.http.metric_exporter") is not None
|
| 21 |
+
)
|
| 22 |
+
if HAS_OTEL_METRICS:
|
| 23 |
+
from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( # type: ignore[import]
|
| 24 |
+
OTLPMetricExporter,
|
| 25 |
+
)
|
| 26 |
+
from opentelemetry.sdk.metrics import MeterProvider # type: ignore[import]
|
| 27 |
+
from opentelemetry.sdk.metrics.export import ( # type: ignore[import]
|
| 28 |
+
PeriodicExportingMetricReader,
|
| 29 |
+
)
|
| 30 |
except ImportError:
|
| 31 |
HAS_OTEL_METRICS = False
|
| 32 |
|
|
|
|
| 164 |
async def shutdown(self) -> None:
|
| 165 |
"""Flush and shut down the underlying providers."""
|
| 166 |
from contextlib import suppress
|
| 167 |
+
|
| 168 |
if self._meter_provider is not None:
|
| 169 |
with suppress(Exception):
|
| 170 |
self._meter_provider.shutdown() # type: ignore[union-attr]
|
hearthnet/relay/push_subscriber.py
CHANGED
|
@@ -101,6 +101,7 @@ class PushSubscriber:
|
|
| 101 |
async def close(self) -> None:
|
| 102 |
"""Close the internal httpx client."""
|
| 103 |
from contextlib import suppress
|
|
|
|
| 104 |
if self._httpx_client is not None:
|
| 105 |
with suppress(Exception):
|
| 106 |
await self._httpx_client.aclose() # type: ignore[union-attr]
|
|
|
|
| 101 |
async def close(self) -> None:
|
| 102 |
"""Close the internal httpx client."""
|
| 103 |
from contextlib import suppress
|
| 104 |
+
|
| 105 |
if self._httpx_client is not None:
|
| 106 |
with suppress(Exception):
|
| 107 |
await self._httpx_client.aclose() # type: ignore[union-attr]
|
hearthnet/services/chat/service.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import uuid
|
| 4 |
-
from datetime import
|
| 5 |
|
| 6 |
from hearthnet.bus.capability import CapabilityDescriptor, RouteRequest
|
| 7 |
|
| 8 |
-
UTC =
|
| 9 |
from hearthnet.services.chat.delivery import DeliveryManager
|
| 10 |
from hearthnet.services.chat.views import ChatView
|
| 11 |
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import uuid
|
| 4 |
+
from datetime import UTC, datetime
|
| 5 |
|
| 6 |
from hearthnet.bus.capability import CapabilityDescriptor, RouteRequest
|
| 7 |
|
| 8 |
+
UTC = UTC
|
| 9 |
from hearthnet.services.chat.delivery import DeliveryManager
|
| 10 |
from hearthnet.services.chat.views import ChatView
|
| 11 |
|
hearthnet/services/chat/thread_views.py
CHANGED
|
@@ -273,15 +273,15 @@ class ThreadViewStore:
|
|
| 273 |
)
|
| 274 |
return result
|
| 275 |
results = []
|
| 276 |
-
for tid,
|
| 277 |
-
if member_id in
|
| 278 |
t = self._threads.get(tid)
|
| 279 |
if t:
|
| 280 |
results.append(
|
| 281 |
Thread(
|
| 282 |
thread_id=t["thread_id"],
|
| 283 |
name=t["name"],
|
| 284 |
-
members=list(
|
| 285 |
created_at=t["created_at"],
|
| 286 |
archived=t["archived"],
|
| 287 |
e2e_enabled=t["e2e_enabled"],
|
|
|
|
| 273 |
)
|
| 274 |
return result
|
| 275 |
results = []
|
| 276 |
+
for tid, member_set in self._members.items():
|
| 277 |
+
if member_id in member_set:
|
| 278 |
t = self._threads.get(tid)
|
| 279 |
if t:
|
| 280 |
results.append(
|
| 281 |
Thread(
|
| 282 |
thread_id=t["thread_id"],
|
| 283 |
name=t["name"],
|
| 284 |
+
members=list(member_set),
|
| 285 |
created_at=t["created_at"],
|
| 286 |
archived=t["archived"],
|
| 287 |
e2e_enabled=t["e2e_enabled"],
|
hearthnet/services/llm/backends/base.py
CHANGED
|
@@ -8,8 +8,9 @@ from typing import Any, Protocol
|
|
| 8 |
@dataclass(frozen=True)
|
| 9 |
class Token:
|
| 10 |
text: str
|
| 11 |
-
logprob: float =
|
| 12 |
stop: bool = False
|
|
|
|
| 13 |
|
| 14 |
|
| 15 |
@dataclass(frozen=True)
|
|
|
|
| 8 |
@dataclass(frozen=True)
|
| 9 |
class Token:
|
| 10 |
text: str
|
| 11 |
+
logprob: float | None = None
|
| 12 |
stop: bool = False
|
| 13 |
+
finish_reason: str | None = None
|
| 14 |
|
| 15 |
|
| 16 |
@dataclass(frozen=True)
|
hearthnet/services/llm/backends/hf_api.py
CHANGED
|
@@ -67,10 +67,12 @@ class HfApiBackend:
|
|
| 67 |
prompt += "\nAssistant:"
|
| 68 |
|
| 69 |
url = f"{self._base_url}/models/{self._model}"
|
| 70 |
-
payload = json.dumps(
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
| 74 |
req = urllib.request.Request( # nosec B310
|
| 75 |
url,
|
| 76 |
data=payload,
|
|
|
|
| 67 |
prompt += "\nAssistant:"
|
| 68 |
|
| 69 |
url = f"{self._base_url}/models/{self._model}"
|
| 70 |
+
payload = json.dumps(
|
| 71 |
+
{
|
| 72 |
+
"inputs": prompt,
|
| 73 |
+
"parameters": {"max_new_tokens": max_tokens, "return_full_text": False},
|
| 74 |
+
}
|
| 75 |
+
).encode()
|
| 76 |
req = urllib.request.Request( # nosec B310
|
| 77 |
url,
|
| 78 |
data=payload,
|
hearthnet/services/llm/backends/llama_cpp.py
CHANGED
|
@@ -30,11 +30,10 @@ class LlamaCppBackend:
|
|
| 30 |
|
| 31 |
def is_available(self) -> bool:
|
| 32 |
try:
|
|
|
|
| 33 |
from pathlib import Path
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
return Path(self._model_path).exists()
|
| 38 |
except ImportError:
|
| 39 |
return False
|
| 40 |
|
|
|
|
| 30 |
|
| 31 |
def is_available(self) -> bool:
|
| 32 |
try:
|
| 33 |
+
from importlib.util import find_spec
|
| 34 |
from pathlib import Path
|
| 35 |
|
| 36 |
+
return Path(self._model_path).exists() and find_spec("llama_cpp") is not None
|
|
|
|
|
|
|
| 37 |
except ImportError:
|
| 38 |
return False
|
| 39 |
|
hearthnet/services/llm/backends/modal_backend.py
CHANGED
|
@@ -61,12 +61,8 @@ class ModalBackend:
|
|
| 61 |
model: str | None = None,
|
| 62 |
api_token: str | None = None,
|
| 63 |
) -> None:
|
| 64 |
-
self._endpoint = (
|
| 65 |
-
|
| 66 |
-
)
|
| 67 |
-
self._model = model or os.getenv(
|
| 68 |
-
"MODAL_MODEL", "HuggingFaceTB/SmolLM2-1.7B-Instruct"
|
| 69 |
-
)
|
| 70 |
self._token = api_token or os.getenv("MODAL_TOKEN", "")
|
| 71 |
self.models: list[BackendModel] = []
|
| 72 |
|
|
|
|
| 61 |
model: str | None = None,
|
| 62 |
api_token: str | None = None,
|
| 63 |
) -> None:
|
| 64 |
+
self._endpoint = (endpoint or os.getenv("MODAL_ENDPOINT", "")).rstrip("/")
|
| 65 |
+
self._model = model or os.getenv("MODAL_MODEL", "HuggingFaceTB/SmolLM2-1.7B-Instruct")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
self._token = api_token or os.getenv("MODAL_TOKEN", "")
|
| 67 |
self.models: list[BackendModel] = []
|
| 68 |
|
hearthnet/services/llm/backends/ollama.py
CHANGED
|
@@ -96,7 +96,10 @@ class OllamaBackend:
|
|
| 96 |
|
| 97 |
import httpx
|
| 98 |
|
| 99 |
-
async with
|
|
|
|
|
|
|
|
|
|
| 100 |
async for line in resp.aiter_lines():
|
| 101 |
if line:
|
| 102 |
try:
|
|
|
|
| 96 |
|
| 97 |
import httpx
|
| 98 |
|
| 99 |
+
async with (
|
| 100 |
+
httpx.AsyncClient(timeout=120.0) as client,
|
| 101 |
+
client.stream("POST", f"{self._base_url}/api/chat", json=payload) as resp,
|
| 102 |
+
):
|
| 103 |
async for line in resp.aiter_lines():
|
| 104 |
if line:
|
| 105 |
try:
|
hearthnet/services/llm/backends/openai_compat.py
CHANGED
|
@@ -106,12 +106,15 @@ class OpenAICompatBackend:
|
|
| 106 |
import httpx
|
| 107 |
|
| 108 |
payload["stream"] = True
|
| 109 |
-
async with
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
| 115 |
async for line in resp.aiter_lines():
|
| 116 |
if line.startswith("data: "):
|
| 117 |
raw = line[6:]
|
|
|
|
| 106 |
import httpx
|
| 107 |
|
| 108 |
payload["stream"] = True
|
| 109 |
+
async with (
|
| 110 |
+
httpx.AsyncClient(timeout=60.0) as client,
|
| 111 |
+
client.stream(
|
| 112 |
+
"POST",
|
| 113 |
+
f"{self._base_url}/chat/completions",
|
| 114 |
+
json=payload,
|
| 115 |
+
headers=headers,
|
| 116 |
+
) as resp,
|
| 117 |
+
):
|
| 118 |
async for line in resp.aiter_lines():
|
| 119 |
if line.startswith("data: "):
|
| 120 |
raw = line[6:]
|
hearthnet/services/llm/service.py
CHANGED
|
@@ -249,18 +249,6 @@ class _EchoBackend:
|
|
| 249 |
def health(self) -> dict:
|
| 250 |
return {"status": "ok", "note": "echo-backend-tests-only"}
|
| 251 |
|
| 252 |
-
async def warm(self) -> None:
|
| 253 |
-
pass
|
| 254 |
-
|
| 255 |
-
async def close(self) -> None:
|
| 256 |
-
pass
|
| 257 |
-
|
| 258 |
-
def health(self) -> dict:
|
| 259 |
-
return {"backend": "echo", "status": "ok"}
|
| 260 |
-
|
| 261 |
-
def is_available(self) -> bool:
|
| 262 |
-
return True
|
| 263 |
-
|
| 264 |
|
| 265 |
def _model_matches(offered: dict, requested: dict) -> bool:
|
| 266 |
req = requested.get("model")
|
|
|
|
| 249 |
def health(self) -> dict:
|
| 250 |
return {"status": "ok", "note": "echo-backend-tests-only"}
|
| 251 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
def _model_matches(offered: dict, requested: dict) -> bool:
|
| 254 |
req = requested.get("model")
|
hearthnet/services/marketplace/post.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
-
from datetime import
|
| 5 |
from typing import Literal
|
| 6 |
|
| 7 |
-
UTC =
|
| 8 |
|
| 9 |
Category = Literal["offer", "request", "info", "emergency"]
|
| 10 |
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
+
from datetime import UTC, datetime
|
| 5 |
from typing import Literal
|
| 6 |
|
| 7 |
+
UTC = UTC
|
| 8 |
|
| 9 |
Category = Literal["offer", "request", "info", "emergency"]
|
| 10 |
|
hearthnet/services/marketplace/service.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import uuid
|
| 4 |
-
from datetime import
|
| 5 |
|
| 6 |
from hearthnet.bus.capability import CapabilityDescriptor, RouteRequest
|
| 7 |
|
| 8 |
-
UTC =
|
| 9 |
from hearthnet.constants import MARKET_DEFAULT_TTL_SECONDS
|
| 10 |
from hearthnet.services.marketplace.views import MarketplaceView
|
| 11 |
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import uuid
|
| 4 |
+
from datetime import UTC, datetime, timedelta
|
| 5 |
|
| 6 |
from hearthnet.bus.capability import CapabilityDescriptor, RouteRequest
|
| 7 |
|
| 8 |
+
UTC = UTC
|
| 9 |
from hearthnet.constants import MARKET_DEFAULT_TTL_SECONDS
|
| 10 |
from hearthnet.services.marketplace.views import MarketplaceView
|
| 11 |
|
hearthnet/services/marketplace/views.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
-
from datetime import
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
-
UTC =
|
| 7 |
|
| 8 |
from hearthnet.services.marketplace.post import Location, Post
|
| 9 |
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
from datetime import UTC, datetime
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
+
UTC = UTC
|
| 7 |
|
| 8 |
from hearthnet.services.marketplace.post import Location, Post
|
| 9 |
|
hearthnet/services/moe/service.py
CHANGED
|
@@ -145,14 +145,14 @@ class MoeService:
|
|
| 145 |
"""Register an expert descriptor.
|
| 146 |
|
| 147 |
input:
|
| 148 |
-
expert_id: str
|
| 149 |
-
expert_type: str
|
| 150 |
-
topic_tags: list[str]
|
| 151 |
-
confidence_score: float
|
| 152 |
community_id: str
|
| 153 |
name: str = ""
|
| 154 |
description: str = ""
|
| 155 |
-
ttl_seconds: float = 3600
|
| 156 |
"""
|
| 157 |
inp = req.body.get("input", {})
|
| 158 |
expert_id = inp.get("expert_id", "")
|
|
|
|
| 145 |
"""Register an expert descriptor.
|
| 146 |
|
| 147 |
input:
|
| 148 |
+
expert_id: str - "human:<NodeID>" | "model:<id>" | "service:<cap>"
|
| 149 |
+
expert_type: str - "human" | "model" | "service" | "external"
|
| 150 |
+
topic_tags: list[str] - topic tags for matching
|
| 151 |
+
confidence_score: float - 0.0-1.0 self-reported
|
| 152 |
community_id: str
|
| 153 |
name: str = ""
|
| 154 |
description: str = ""
|
| 155 |
+
ttl_seconds: float = 3600 - 0 = never expires
|
| 156 |
"""
|
| 157 |
inp = req.body.get("input", {})
|
| 158 |
expert_id = inp.get("expert_id", "")
|
hearthnet/services/protocol/service.py
CHANGED
|
@@ -43,8 +43,25 @@ _SUITE_V1: list[tuple[str, tuple[int, int], dict, str]] = [
|
|
| 43 |
("file.put", (1, 0), {"input": {"data_b64": "cGluZw==", "filename": "ping.txt"}}, "cid"),
|
| 44 |
("file.list", (1, 0), {"input": {}}, "files"),
|
| 45 |
("market.list", (1, 0), {"input": {}}, "posts"),
|
| 46 |
-
(
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
("moe.list", (1, 0), {"input": {}}, "experts"),
|
| 49 |
("moe.route", (1, 0), {"input": {"query": "ping"}}, "candidates"),
|
| 50 |
("model.list", (1, 0), {"input": {}}, "models"),
|
|
@@ -55,12 +72,30 @@ _SUITE_V2: list[tuple[str, tuple[int, int], dict, str]] = [
|
|
| 55 |
# Phase 2 β only checked if those services are registered
|
| 56 |
("ocr.image", (1, 0), {"input": {"image_cid": "blake3:test"}}, ""),
|
| 57 |
("trans.text", (1, 0), {"input": {"text": "hello", "from": "en", "to": "de"}}, ""),
|
| 58 |
-
(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
]
|
| 60 |
|
| 61 |
_SUITE_V3: list[tuple[str, tuple[int, int], dict, str]] = [
|
| 62 |
# Phase 3 experimental
|
| 63 |
-
(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
("tool.plant_identify", (1, 0), {"input": {}}, ""), # expects error: bad_request
|
| 65 |
]
|
| 66 |
|
|
@@ -145,9 +180,7 @@ class ProtocolService:
|
|
| 145 |
},
|
| 146 |
"started": bool(self._node and getattr(self._node, "_started", False)),
|
| 147 |
"event_log_head": (
|
| 148 |
-
self._node._event_log.head()
|
| 149 |
-
if self._node and self._node._event_log
|
| 150 |
-
else None
|
| 151 |
),
|
| 152 |
},
|
| 153 |
"meta": {"ms": 0},
|
|
@@ -187,7 +220,9 @@ class ProtocolService:
|
|
| 187 |
|
| 188 |
for cap_name, version_req, body, expected_field in checks:
|
| 189 |
if bus is None:
|
| 190 |
-
results.append(
|
|
|
|
|
|
|
| 191 |
skipped += 1
|
| 192 |
continue
|
| 193 |
|
|
@@ -196,7 +231,14 @@ class ProtocolService:
|
|
| 196 |
try:
|
| 197 |
local = bus.registry.find(cap_name, version_req)
|
| 198 |
if not local:
|
| 199 |
-
results.append(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
skipped += 1
|
| 201 |
continue
|
| 202 |
except Exception:
|
|
@@ -206,9 +248,13 @@ class ProtocolService:
|
|
| 206 |
result = await bus.call(cap_name, version_req, body)
|
| 207 |
# A capability passes if it doesn't return a top-level "error" key
|
| 208 |
# AND (if expected_field is set) the output contains that field.
|
| 209 |
-
has_error =
|
| 210 |
-
"
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
)
|
| 213 |
output_ok = True
|
| 214 |
if expected_field and not has_error:
|
|
@@ -218,13 +264,24 @@ class ProtocolService:
|
|
| 218 |
|
| 219 |
if has_error:
|
| 220 |
error_msg = result.get("error", result.get("message", "unknown"))
|
| 221 |
-
results.append(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
failed += 1
|
| 223 |
else:
|
| 224 |
-
results.append(
|
|
|
|
|
|
|
| 225 |
passed += 1
|
| 226 |
except Exception as exc:
|
| 227 |
-
results.append(
|
|
|
|
|
|
|
| 228 |
failed += 1
|
| 229 |
|
| 230 |
duration_ms = round((time.time() - t0) * 1000, 1)
|
|
|
|
| 43 |
("file.put", (1, 0), {"input": {"data_b64": "cGluZw==", "filename": "ping.txt"}}, "cid"),
|
| 44 |
("file.list", (1, 0), {"input": {}}, "files"),
|
| 45 |
("market.list", (1, 0), {"input": {}}, "posts"),
|
| 46 |
+
(
|
| 47 |
+
"market.post",
|
| 48 |
+
(1, 0),
|
| 49 |
+
{
|
| 50 |
+
"input": {
|
| 51 |
+
"title": "__conformance__",
|
| 52 |
+
"body": "test",
|
| 53 |
+
"category": "other",
|
| 54 |
+
"client_id": "__x09__",
|
| 55 |
+
}
|
| 56 |
+
},
|
| 57 |
+
"",
|
| 58 |
+
),
|
| 59 |
+
(
|
| 60 |
+
"chat.send",
|
| 61 |
+
(1, 0),
|
| 62 |
+
{"input": {"to": "self", "body": "ping", "client_id": "__x09_chat__"}},
|
| 63 |
+
"",
|
| 64 |
+
),
|
| 65 |
("moe.list", (1, 0), {"input": {}}, "experts"),
|
| 66 |
("moe.route", (1, 0), {"input": {"query": "ping"}}, "candidates"),
|
| 67 |
("model.list", (1, 0), {"input": {}}, "models"),
|
|
|
|
| 72 |
# Phase 2 β only checked if those services are registered
|
| 73 |
("ocr.image", (1, 0), {"input": {"image_cid": "blake3:test"}}, ""),
|
| 74 |
("trans.text", (1, 0), {"input": {"text": "hello", "from": "en", "to": "de"}}, ""),
|
| 75 |
+
(
|
| 76 |
+
"rerank.text",
|
| 77 |
+
(1, 0),
|
| 78 |
+
{"input": {"query": "test", "documents": [{"id": "d1", "text": "test"}]}},
|
| 79 |
+
"",
|
| 80 |
+
),
|
| 81 |
]
|
| 82 |
|
| 83 |
_SUITE_V3: list[tuple[str, tuple[int, int], dict, str]] = [
|
| 84 |
# Phase 3 experimental
|
| 85 |
+
(
|
| 86 |
+
"moe.register",
|
| 87 |
+
(1, 0),
|
| 88 |
+
{
|
| 89 |
+
"input": {
|
| 90 |
+
"expert_id": "model:x09",
|
| 91 |
+
"expert_type": "model",
|
| 92 |
+
"topic_tags": ["test"],
|
| 93 |
+
"confidence_score": 0.5,
|
| 94 |
+
"community_id": "test",
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
"registered",
|
| 98 |
+
),
|
| 99 |
("tool.plant_identify", (1, 0), {"input": {}}, ""), # expects error: bad_request
|
| 100 |
]
|
| 101 |
|
|
|
|
| 180 |
},
|
| 181 |
"started": bool(self._node and getattr(self._node, "_started", False)),
|
| 182 |
"event_log_head": (
|
| 183 |
+
self._node._event_log.head() if self._node and self._node._event_log else None
|
|
|
|
|
|
|
| 184 |
),
|
| 185 |
},
|
| 186 |
"meta": {"ms": 0},
|
|
|
|
| 220 |
|
| 221 |
for cap_name, version_req, body, expected_field in checks:
|
| 222 |
if bus is None:
|
| 223 |
+
results.append(
|
| 224 |
+
{"capability": cap_name, "passed": False, "skipped": True, "error": "no_bus"}
|
| 225 |
+
)
|
| 226 |
skipped += 1
|
| 227 |
continue
|
| 228 |
|
|
|
|
| 231 |
try:
|
| 232 |
local = bus.registry.find(cap_name, version_req)
|
| 233 |
if not local:
|
| 234 |
+
results.append(
|
| 235 |
+
{
|
| 236 |
+
"capability": cap_name,
|
| 237 |
+
"passed": False,
|
| 238 |
+
"skipped": True,
|
| 239 |
+
"error": "not_registered",
|
| 240 |
+
}
|
| 241 |
+
)
|
| 242 |
skipped += 1
|
| 243 |
continue
|
| 244 |
except Exception:
|
|
|
|
| 248 |
result = await bus.call(cap_name, version_req, body)
|
| 249 |
# A capability passes if it doesn't return a top-level "error" key
|
| 250 |
# AND (if expected_field is set) the output contains that field.
|
| 251 |
+
has_error = (
|
| 252 |
+
"error" in result
|
| 253 |
+
and result["error"]
|
| 254 |
+
not in (
|
| 255 |
+
"bad_request", # some capabilities intentionally return bad_request for empty input
|
| 256 |
+
None,
|
| 257 |
+
)
|
| 258 |
)
|
| 259 |
output_ok = True
|
| 260 |
if expected_field and not has_error:
|
|
|
|
| 264 |
|
| 265 |
if has_error:
|
| 266 |
error_msg = result.get("error", result.get("message", "unknown"))
|
| 267 |
+
results.append(
|
| 268 |
+
{
|
| 269 |
+
"capability": cap_name,
|
| 270 |
+
"passed": False,
|
| 271 |
+
"skipped": False,
|
| 272 |
+
"error": str(error_msg),
|
| 273 |
+
}
|
| 274 |
+
)
|
| 275 |
failed += 1
|
| 276 |
else:
|
| 277 |
+
results.append(
|
| 278 |
+
{"capability": cap_name, "passed": True, "skipped": False, "error": ""}
|
| 279 |
+
)
|
| 280 |
passed += 1
|
| 281 |
except Exception as exc:
|
| 282 |
+
results.append(
|
| 283 |
+
{"capability": cap_name, "passed": False, "skipped": False, "error": str(exc)}
|
| 284 |
+
)
|
| 285 |
failed += 1
|
| 286 |
|
| 287 |
duration_ms = round((time.time() - t0) * 1000, 1)
|
hearthnet/services/rag/federated.py
CHANGED
|
@@ -21,7 +21,7 @@ from hearthnet.bus.capability import CapabilityDescriptor, RouteRequest
|
|
| 21 |
|
| 22 |
_log = logging.getLogger(__name__)
|
| 23 |
|
| 24 |
-
_DEFAULT_CONFIDENCE = 0.5
|
| 25 |
_DEFAULT_FANOUT_TIMEOUT = 4.0 # seconds per remote call (B)
|
| 26 |
_DEFAULT_K = 5
|
| 27 |
|
|
@@ -93,14 +93,10 @@ class FederatedRagService:
|
|
| 93 |
return {"output": {"chunks": []}, "meta": {"corpus": corpus, "federated": False}}
|
| 94 |
|
| 95 |
# ββ Strategy C: local-first ββββββββββββββββββββββββββββββββββββββββ
|
| 96 |
-
local_chunks, local_node_id, best_local_score = await self._query_local(
|
| 97 |
-
query, k, corpus
|
| 98 |
-
)
|
| 99 |
|
| 100 |
if best_local_score >= threshold and local_chunks:
|
| 101 |
-
_log.debug(
|
| 102 |
-
"federated_query: local-first short-circuit score=%.3f", best_local_score
|
| 103 |
-
)
|
| 104 |
_add_source(local_chunks, local_node_id)
|
| 105 |
return {
|
| 106 |
"output": {"chunks": local_chunks[:k]},
|
|
@@ -132,11 +128,13 @@ class FederatedRagService:
|
|
| 132 |
|
| 133 |
# Reorder by MoE priority if we got one
|
| 134 |
if peer_priority:
|
|
|
|
| 135 |
def _priority_key(item: tuple[str, dict]) -> int:
|
| 136 |
try:
|
| 137 |
return peer_priority.index(item[0])
|
| 138 |
except ValueError:
|
| 139 |
return len(peer_priority)
|
|
|
|
| 140 |
all_results.sort(key=_priority_key)
|
| 141 |
|
| 142 |
# ββ Merge local + remote βββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -159,9 +157,7 @@ class FederatedRagService:
|
|
| 159 |
rerank_body = {
|
| 160 |
"input": {
|
| 161 |
"query": query,
|
| 162 |
-
"docs": [
|
| 163 |
-
{"id": str(i), "text": c["text"]} for i, c in enumerate(merged)
|
| 164 |
-
],
|
| 165 |
"top_k": k,
|
| 166 |
}
|
| 167 |
}
|
|
@@ -212,9 +208,7 @@ class FederatedRagService:
|
|
| 212 |
_log.debug("local rag.query failed: %s", exc)
|
| 213 |
return [], self._bus.node_id_full, 0.0
|
| 214 |
|
| 215 |
-
async def _moe_peer_priority(
|
| 216 |
-
self, query: str, corpus: str | None
|
| 217 |
-
) -> list[str] | None:
|
| 218 |
"""Ask moe.route to rank which expert peers to prefer. Returns node_ids or None."""
|
| 219 |
tags = [corpus] if corpus else []
|
| 220 |
try:
|
|
@@ -233,6 +227,7 @@ class FederatedRagService:
|
|
| 233 |
# Utilities
|
| 234 |
# ---------------------------------------------------------------------------
|
| 235 |
|
|
|
|
| 236 |
def _add_source(chunks: list[dict], node_id: str) -> None:
|
| 237 |
"""Attach source_node provenance to each chunk in-place."""
|
| 238 |
for chunk in chunks:
|
|
|
|
| 21 |
|
| 22 |
_log = logging.getLogger(__name__)
|
| 23 |
|
| 24 |
+
_DEFAULT_CONFIDENCE = 0.5 # local-first threshold (C)
|
| 25 |
_DEFAULT_FANOUT_TIMEOUT = 4.0 # seconds per remote call (B)
|
| 26 |
_DEFAULT_K = 5
|
| 27 |
|
|
|
|
| 93 |
return {"output": {"chunks": []}, "meta": {"corpus": corpus, "federated": False}}
|
| 94 |
|
| 95 |
# ββ Strategy C: local-first ββββββββββββββββββββββββββββββββββββββββ
|
| 96 |
+
local_chunks, local_node_id, best_local_score = await self._query_local(query, k, corpus)
|
|
|
|
|
|
|
| 97 |
|
| 98 |
if best_local_score >= threshold and local_chunks:
|
| 99 |
+
_log.debug("federated_query: local-first short-circuit score=%.3f", best_local_score)
|
|
|
|
|
|
|
| 100 |
_add_source(local_chunks, local_node_id)
|
| 101 |
return {
|
| 102 |
"output": {"chunks": local_chunks[:k]},
|
|
|
|
| 128 |
|
| 129 |
# Reorder by MoE priority if we got one
|
| 130 |
if peer_priority:
|
| 131 |
+
|
| 132 |
def _priority_key(item: tuple[str, dict]) -> int:
|
| 133 |
try:
|
| 134 |
return peer_priority.index(item[0])
|
| 135 |
except ValueError:
|
| 136 |
return len(peer_priority)
|
| 137 |
+
|
| 138 |
all_results.sort(key=_priority_key)
|
| 139 |
|
| 140 |
# ββ Merge local + remote βββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 157 |
rerank_body = {
|
| 158 |
"input": {
|
| 159 |
"query": query,
|
| 160 |
+
"docs": [{"id": str(i), "text": c["text"]} for i, c in enumerate(merged)],
|
|
|
|
|
|
|
| 161 |
"top_k": k,
|
| 162 |
}
|
| 163 |
}
|
|
|
|
| 208 |
_log.debug("local rag.query failed: %s", exc)
|
| 209 |
return [], self._bus.node_id_full, 0.0
|
| 210 |
|
| 211 |
+
async def _moe_peer_priority(self, query: str, corpus: str | None) -> list[str] | None:
|
|
|
|
|
|
|
| 212 |
"""Ask moe.route to rank which expert peers to prefer. Returns node_ids or None."""
|
| 213 |
tags = [corpus] if corpus else []
|
| 214 |
try:
|
|
|
|
| 227 |
# Utilities
|
| 228 |
# ---------------------------------------------------------------------------
|
| 229 |
|
| 230 |
+
|
| 231 |
def _add_source(chunks: list[dict], node_id: str) -> None:
|
| 232 |
"""Attach source_node provenance to each chunk in-place."""
|
| 233 |
for chunk in chunks:
|
hearthnet/services/rag/replication.py
CHANGED
|
@@ -72,9 +72,7 @@ class CorpusReplicator:
|
|
| 72 |
_log.info("CorpusReplicator started (local_node=%s)", self._local_node_id[:16])
|
| 73 |
try:
|
| 74 |
async for event in self._event_log.subscribe(["rag.document.ingested"]):
|
| 75 |
-
asyncio.create_task(
|
| 76 |
-
self._handle_event(event), name="corpus-repl-event"
|
| 77 |
-
)
|
| 78 |
except asyncio.CancelledError:
|
| 79 |
_log.info("CorpusReplicator stopped")
|
| 80 |
raise
|
|
|
|
| 72 |
_log.info("CorpusReplicator started (local_node=%s)", self._local_node_id[:16])
|
| 73 |
try:
|
| 74 |
async for event in self._event_log.subscribe(["rag.document.ingested"]):
|
| 75 |
+
asyncio.create_task(self._handle_event(event), name="corpus-repl-event")
|
|
|
|
|
|
|
| 76 |
except asyncio.CancelledError:
|
| 77 |
_log.info("CorpusReplicator stopped")
|
| 78 |
raise
|
hearthnet/services/rag/service.py
CHANGED
|
@@ -124,11 +124,7 @@ class RagService:
|
|
| 124 |
# Emit rag.document.ingested event so peers learn a new doc exists (X02).
|
| 125 |
if not result.was_duplicate and self._event_log is not None:
|
| 126 |
try:
|
| 127 |
-
author =
|
| 128 |
-
self._bus.node_id_full
|
| 129 |
-
if self._bus is not None
|
| 130 |
-
else "unknown"
|
| 131 |
-
)
|
| 132 |
payload: dict = {
|
| 133 |
"corpus": self._corpus,
|
| 134 |
"doc_cid": result.doc_cid,
|
|
|
|
| 124 |
# Emit rag.document.ingested event so peers learn a new doc exists (X02).
|
| 125 |
if not result.was_duplicate and self._event_log is not None:
|
| 126 |
try:
|
| 127 |
+
author = self._bus.node_id_full if self._bus is not None else "unknown"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
payload: dict = {
|
| 129 |
"corpus": self._corpus,
|
| 130 |
"doc_cid": result.doc_cid,
|
hearthnet/services/speech/backends/base.py
CHANGED
|
@@ -61,7 +61,7 @@ class TtsBackend(Protocol):
|
|
| 61 |
text: str,
|
| 62 |
voice: str | None = None,
|
| 63 |
language: str = "de",
|
| 64 |
-
|
| 65 |
) -> TtsResult: ...
|
| 66 |
|
| 67 |
def health(self) -> dict: ...
|
|
|
|
| 61 |
text: str,
|
| 62 |
voice: str | None = None,
|
| 63 |
language: str = "de",
|
| 64 |
+
audio_format: str = "ogg_vorbis",
|
| 65 |
) -> TtsResult: ...
|
| 66 |
|
| 67 |
def health(self) -> dict: ...
|
hearthnet/services/speech/backends/edge_tts.py
CHANGED
|
@@ -33,7 +33,7 @@ class EdgeTtsBackend:
|
|
| 33 |
text: str,
|
| 34 |
voice: str | None = "de-DE-KatjaNeural",
|
| 35 |
language: str = "de",
|
| 36 |
-
|
| 37 |
) -> Any:
|
| 38 |
from hearthnet.services.speech.backends.base import TtsResult
|
| 39 |
|
|
|
|
| 33 |
text: str,
|
| 34 |
voice: str | None = "de-DE-KatjaNeural",
|
| 35 |
language: str = "de",
|
| 36 |
+
audio_format: str = "ogg_vorbis",
|
| 37 |
) -> Any:
|
| 38 |
from hearthnet.services.speech.backends.base import TtsResult
|
| 39 |
|
hearthnet/services/tools/plant.py
CHANGED
|
@@ -152,9 +152,7 @@ class PlantIdentificationService:
|
|
| 152 |
# Backend: local vision.describe + llm.complete
|
| 153 |
# ------------------------------------------------------------------
|
| 154 |
|
| 155 |
-
async def _try_local_vision(
|
| 156 |
-
self, image_b64: str, hints: list[str]
|
| 157 |
-
) -> dict | None:
|
| 158 |
if self._bus is None:
|
| 159 |
return None
|
| 160 |
|
|
@@ -179,9 +177,7 @@ class PlantIdentificationService:
|
|
| 179 |
return None
|
| 180 |
|
| 181 |
description_raw = (
|
| 182 |
-
desc_resp.get("output", {}).get("description", "")
|
| 183 |
-
or desc_resp.get("output", "")
|
| 184 |
-
or ""
|
| 185 |
)
|
| 186 |
if not description_raw:
|
| 187 |
return None
|
|
@@ -214,20 +210,14 @@ class PlantIdentificationService:
|
|
| 214 |
"care_tips": [],
|
| 215 |
}
|
| 216 |
|
| 217 |
-
text = (
|
| 218 |
-
llm_resp.get("output", {}).get("text", "")
|
| 219 |
-
or llm_resp.get("output", "")
|
| 220 |
-
or ""
|
| 221 |
-
)
|
| 222 |
return _parse_llm_json(text, description_raw)
|
| 223 |
|
| 224 |
# ------------------------------------------------------------------
|
| 225 |
# Backend: HF Inference API
|
| 226 |
# ------------------------------------------------------------------
|
| 227 |
|
| 228 |
-
async def _try_hf_api(
|
| 229 |
-
self, image_bytes: bytes, hints: list[str], token: str
|
| 230 |
-
) -> dict | None:
|
| 231 |
"""Call the public plant.id HF Space via the Inference API.
|
| 232 |
|
| 233 |
The space used is: 'hf-vision/plant-identification' if it exists;
|
|
@@ -288,7 +278,7 @@ class PlantIdentificationService:
|
|
| 288 |
|
| 289 |
|
| 290 |
def _build_parse_prompt(description: str, hints: list[str]) -> str:
|
| 291 |
-
hints_text =
|
| 292 |
return f"""You are a botanist. Based on this plant description, return a JSON object with these fields:
|
| 293 |
- name: latin binomial (string, e.g. "Urtica dioica") or "Unknown"
|
| 294 |
- common_name: common English name (string)
|
|
|
|
| 152 |
# Backend: local vision.describe + llm.complete
|
| 153 |
# ------------------------------------------------------------------
|
| 154 |
|
| 155 |
+
async def _try_local_vision(self, image_b64: str, hints: list[str]) -> dict | None:
|
|
|
|
|
|
|
| 156 |
if self._bus is None:
|
| 157 |
return None
|
| 158 |
|
|
|
|
| 177 |
return None
|
| 178 |
|
| 179 |
description_raw = (
|
| 180 |
+
desc_resp.get("output", {}).get("description", "") or desc_resp.get("output", "") or ""
|
|
|
|
|
|
|
| 181 |
)
|
| 182 |
if not description_raw:
|
| 183 |
return None
|
|
|
|
| 210 |
"care_tips": [],
|
| 211 |
}
|
| 212 |
|
| 213 |
+
text = llm_resp.get("output", {}).get("text", "") or llm_resp.get("output", "") or ""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
return _parse_llm_json(text, description_raw)
|
| 215 |
|
| 216 |
# ------------------------------------------------------------------
|
| 217 |
# Backend: HF Inference API
|
| 218 |
# ------------------------------------------------------------------
|
| 219 |
|
| 220 |
+
async def _try_hf_api(self, image_bytes: bytes, hints: list[str], token: str) -> dict | None:
|
|
|
|
|
|
|
| 221 |
"""Call the public plant.id HF Space via the Inference API.
|
| 222 |
|
| 223 |
The space used is: 'hf-vision/plant-identification' if it exists;
|
|
|
|
| 278 |
|
| 279 |
|
| 280 |
def _build_parse_prompt(description: str, hints: list[str]) -> str:
|
| 281 |
+
hints_text = f"\nAdditional context: {', '.join(hints)}" if hints else ""
|
| 282 |
return f"""You are a botanist. Based on this plant description, return a JSON object with these fields:
|
| 283 |
- name: latin binomial (string, e.g. "Urtica dioica") or "Unknown"
|
| 284 |
- common_name: common English name (string)
|
hearthnet/transport/backpressure.py
CHANGED
|
@@ -92,6 +92,7 @@ class RateCheck:
|
|
| 92 |
|
| 93 |
def check(self, now: float | None = None) -> bool:
|
| 94 |
import time
|
|
|
|
| 95 |
t = now if now is not None else time.monotonic()
|
| 96 |
cutoff = t - self._window
|
| 97 |
self._calls = [c for c in self._calls if c > cutoff]
|
|
@@ -122,6 +123,7 @@ class RateLimiter:
|
|
| 122 |
|
| 123 |
async def acquire(self) -> None:
|
| 124 |
import time
|
|
|
|
| 125 |
while True:
|
| 126 |
async with self._lock:
|
| 127 |
t = time.monotonic()
|
|
|
|
| 92 |
|
| 93 |
def check(self, now: float | None = None) -> bool:
|
| 94 |
import time
|
| 95 |
+
|
| 96 |
t = now if now is not None else time.monotonic()
|
| 97 |
cutoff = t - self._window
|
| 98 |
self._calls = [c for c in self._calls if c > cutoff]
|
|
|
|
| 123 |
|
| 124 |
async def acquire(self) -> None:
|
| 125 |
import time
|
| 126 |
+
|
| 127 |
while True:
|
| 128 |
async with self._lock:
|
| 129 |
t = time.monotonic()
|
hearthnet/transport/client.py
CHANGED
|
@@ -7,9 +7,9 @@ import json
|
|
| 7 |
import secrets
|
| 8 |
from collections.abc import AsyncIterator
|
| 9 |
from dataclasses import dataclass, field
|
| 10 |
-
from datetime import
|
| 11 |
|
| 12 |
-
UTC =
|
| 13 |
|
| 14 |
try:
|
| 15 |
import httpx
|
|
@@ -110,12 +110,15 @@ class HttpClient:
|
|
| 110 |
headers = self._make_headers(payload)
|
| 111 |
headers["Accept"] = "text/event-stream"
|
| 112 |
try:
|
| 113 |
-
async with
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
| 119 |
async for line in resp.aiter_lines():
|
| 120 |
if line.startswith("data: "):
|
| 121 |
with contextlib.suppress(json.JSONDecodeError):
|
|
|
|
| 7 |
import secrets
|
| 8 |
from collections.abc import AsyncIterator
|
| 9 |
from dataclasses import dataclass, field
|
| 10 |
+
from datetime import UTC, datetime
|
| 11 |
|
| 12 |
+
UTC = UTC
|
| 13 |
|
| 14 |
try:
|
| 15 |
import httpx
|
|
|
|
| 110 |
headers = self._make_headers(payload)
|
| 111 |
headers["Accept"] = "text/event-stream"
|
| 112 |
try:
|
| 113 |
+
async with (
|
| 114 |
+
httpx.AsyncClient(verify=self._verify_ssl) as client,
|
| 115 |
+
client.stream(
|
| 116 |
+
"POST",
|
| 117 |
+
f"{self._base_url}/bus/v1/call",
|
| 118 |
+
json=payload,
|
| 119 |
+
headers=headers,
|
| 120 |
+
) as resp,
|
| 121 |
+
):
|
| 122 |
async for line in resp.aiter_lines():
|
| 123 |
if line.startswith("data: "):
|
| 124 |
with contextlib.suppress(json.JSONDecodeError):
|
hearthnet/transport/server.py
CHANGED
|
@@ -21,10 +21,10 @@ from __future__ import annotations
|
|
| 21 |
|
| 22 |
import asyncio
|
| 23 |
from collections.abc import Callable
|
| 24 |
-
from datetime import
|
| 25 |
from typing import Any
|
| 26 |
|
| 27 |
-
UTC =
|
| 28 |
|
| 29 |
try:
|
| 30 |
import uvicorn
|
|
|
|
| 21 |
|
| 22 |
import asyncio
|
| 23 |
from collections.abc import Callable
|
| 24 |
+
from datetime import UTC, datetime
|
| 25 |
from typing import Any
|
| 26 |
|
| 27 |
+
UTC = UTC
|
| 28 |
|
| 29 |
try:
|
| 30 |
import uvicorn
|
hearthnet/transport/websocket.py
CHANGED
|
@@ -24,6 +24,10 @@ except ImportError:
|
|
| 24 |
HAS_WEBSOCKETS = False
|
| 25 |
|
| 26 |
# Optional FastAPI/Starlette WebSocket import (server-side)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
try:
|
| 28 |
from starlette.websockets import ( # type: ignore[import]
|
| 29 |
WebSocket,
|
|
|
|
| 24 |
HAS_WEBSOCKETS = False
|
| 25 |
|
| 26 |
# Optional FastAPI/Starlette WebSocket import (server-side)
|
| 27 |
+
WebSocket: Any
|
| 28 |
+
WebSocketDisconnect: Any
|
| 29 |
+
WebSocketState: Any
|
| 30 |
+
|
| 31 |
try:
|
| 32 |
from starlette.websockets import ( # type: ignore[import]
|
| 33 |
WebSocket,
|
hearthnet/types.py
CHANGED
|
@@ -46,16 +46,16 @@ class HearthNetError(Exception):
|
|
| 46 |
# ββ Phase 3 type aliases βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 47 |
|
| 48 |
|
| 49 |
-
ShardID = NewType("ShardID", str)
|
| 50 |
-
ExpertID = NewType("ExpertID", str)
|
| 51 |
ExpertKind = Literal["human", "model", "service", "external"]
|
| 52 |
-
ClaimID = NewType("ClaimID", str)
|
| 53 |
SourceID = NewType("SourceID", str)
|
| 54 |
EvidenceLevel = Literal["unverified", "cited", "cross_referenced", "attested", "disputed"]
|
| 55 |
-
RoundID = NewType("RoundID", str)
|
| 56 |
-
LoraBeaconID = NewType("LoraBeaconID", str)
|
| 57 |
LoraDeviceID = NewType("LoraDeviceID", str)
|
| 58 |
-
AlertID = NewType("AlertID", str)
|
| 59 |
AlertSeverity = Literal["info", "advisory", "warning", "emergency", "extreme"]
|
| 60 |
AckStatus = Literal["received", "acting", "need_help", "standing_down", "mistaken"]
|
| 61 |
|
|
|
|
| 46 |
# ββ Phase 3 type aliases βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 47 |
|
| 48 |
|
| 49 |
+
ShardID = NewType("ShardID", str) # "<model_id>:<lo>-<hi>[:tier]"
|
| 50 |
+
ExpertID = NewType("ExpertID", str) # "human:..." | "model:..." | "service:..." | "external:..."
|
| 51 |
ExpertKind = Literal["human", "model", "service", "external"]
|
| 52 |
+
ClaimID = NewType("ClaimID", str) # base32 of SHA-256 canonical claim
|
| 53 |
SourceID = NewType("SourceID", str)
|
| 54 |
EvidenceLevel = Literal["unverified", "cited", "cross_referenced", "attested", "disputed"]
|
| 55 |
+
RoundID = NewType("RoundID", str) # ULID β fedlearn round
|
| 56 |
+
LoraBeaconID = NewType("LoraBeaconID", str) # 8-byte hex, hardware-issued
|
| 57 |
LoraDeviceID = NewType("LoraDeviceID", str)
|
| 58 |
+
AlertID = NewType("AlertID", str) # ULID
|
| 59 |
AlertSeverity = Literal["info", "advisory", "warning", "emergency", "extreme"]
|
| 60 |
AckStatus = Literal["received", "acting", "need_help", "standing_down", "mistaken"]
|
| 61 |
|