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
Files changed (50) hide show
  1. app.py +1 -0
  2. hearthnet/bus/registry.py +2 -0
  3. hearthnet/bus/trace.py +1 -0
  4. hearthnet/civdef/service.py +0 -1
  5. hearthnet/cli.py +34 -14
  6. hearthnet/config.py +4 -3
  7. hearthnet/conformance/runner.py +193 -36
  8. hearthnet/constants.py +1 -1
  9. hearthnet/discovery/mdns.py +7 -4
  10. hearthnet/discovery/peers.py +5 -4
  11. hearthnet/discovery/udp.py +3 -0
  12. hearthnet/emergency/state.py +5 -1
  13. hearthnet/events/log.py +2 -2
  14. hearthnet/events/replay.py +2 -2
  15. hearthnet/events/snapshot.py +3 -3
  16. hearthnet/evidence/service.py +1 -3
  17. hearthnet/identity/keys.py +1 -0
  18. hearthnet/identity/manifest.py +5 -2
  19. hearthnet/node.py +1 -1
  20. hearthnet/observability/doctor.py +0 -1
  21. hearthnet/observability/federated.py +1 -3
  22. hearthnet/observability/logging.py +1 -3
  23. hearthnet/observability/metrics.py +18 -14
  24. hearthnet/observability/otlp_export.py +14 -9
  25. hearthnet/relay/push_subscriber.py +1 -0
  26. hearthnet/services/chat/service.py +2 -2
  27. hearthnet/services/chat/thread_views.py +3 -3
  28. hearthnet/services/llm/backends/base.py +2 -1
  29. hearthnet/services/llm/backends/hf_api.py +6 -4
  30. hearthnet/services/llm/backends/llama_cpp.py +2 -3
  31. hearthnet/services/llm/backends/modal_backend.py +2 -6
  32. hearthnet/services/llm/backends/ollama.py +4 -1
  33. hearthnet/services/llm/backends/openai_compat.py +9 -6
  34. hearthnet/services/llm/service.py +0 -12
  35. hearthnet/services/marketplace/post.py +2 -2
  36. hearthnet/services/marketplace/service.py +2 -2
  37. hearthnet/services/marketplace/views.py +2 -2
  38. hearthnet/services/moe/service.py +5 -5
  39. hearthnet/services/protocol/service.py +72 -15
  40. hearthnet/services/rag/federated.py +8 -13
  41. hearthnet/services/rag/replication.py +1 -3
  42. hearthnet/services/rag/service.py +1 -5
  43. hearthnet/services/speech/backends/base.py +1 -1
  44. hearthnet/services/speech/backends/edge_tts.py +1 -1
  45. hearthnet/services/tools/plant.py +5 -15
  46. hearthnet/transport/backpressure.py +2 -0
  47. hearthnet/transport/client.py +11 -8
  48. hearthnet/transport/server.py +2 -2
  49. hearthnet/transport/websocket.py +4 -0
  50. 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) < ["DEBUG", "INFO", "WARNING", "ERROR"].index(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("--keep-keys", is_flag=True, help="Keep Ed25519 identity keys, erase everything else.")
 
 
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(host, port, "rag.ingest", (1, 0), {
522
- "input": {"corpus": corpus, "filename": f.name, "data_b64": data_b64}
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(host, port, "community.invite", (1, 0), {
579
- "input": {"invitee_node_id": node_id, "initial_level": level, "ttl_seconds": ttl}
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(host, port, "community.redeem", (1, 0), {
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
- f.stat().st_size for f in model_dir.rglob("*") if f.is_file()
754
- ) / (1024 * 1024)
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
- from contextlib import suppress
546
- with suppress(OSError):
547
- os.unlink(tmp)
 
 
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 # "1.0", "2.0", "3.0"
23
  expected_output_fields: list[str] = field(default_factory=list)
24
- expect_error: str | None = None # if set, pass only when this error is returned
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("protocol.version.list", (1, 0), {"input": {}}, "1.0", ["contract_versions"], description="protocol.version.list returns supported versions"),
32
- Check("protocol.conformance.report", (1, 0), {"input": {"suite_version": "1.0", "fast": True}}, "1.0", ["passed", "total"], description="protocol.conformance.report can self-report"),
33
-
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  # Embedding
35
- Check("embed.text", (1, 0), {"input": {"texts": ["conformance ping"]}}, "1.0", ["vectors"], description="embed.text returns vectors"),
36
-
 
 
 
 
 
 
37
  # RAG
38
- Check("rag.query", (1, 0), {"input": {"query": "ping", "corpus": "demo", "k": 1}}, "1.0", [], description="rag.query responds"),
39
- Check("rag.list_corpora", (1, 0), {"input": {}}, "1.0", ["corpora"], description="rag.list_corpora returns list"),
40
-
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  # Files
42
- Check("file.list", (1, 0), {"input": {}}, "1.0", ["files"], description="file.list returns files list"),
43
- Check("file.put", (1, 0), {"input": {"data_b64": "aGVsbG8=", "filename": "x09.txt"}}, "1.0", ["cid"], description="file.put returns cid"),
44
-
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  # Marketplace
46
- Check("market.list", (1, 0), {"input": {}}, "1.0", ["posts"], description="market.list returns posts"),
47
-
 
 
 
 
 
 
48
  # LLM
49
- Check("llm.complete", (1, 0), {"input": {"prompt": "x09 conformance", "max_tokens": 1}}, "1.0", [], description="llm.complete responds"),
50
-
 
 
 
 
 
 
51
  # Chat
52
- Check("chat.send", (1, 0), {"input": {"to": "self", "body": "x09", "client_id": "x09_conformance"}}, "1.0", [], description="chat.send accepts message"),
53
-
 
 
 
 
 
 
54
  # MoE (Phase 3 but bus-registered in all nodes)
55
- Check("moe.list", (1, 0), {"input": {}}, "1.0", ["experts"], description="moe.list returns experts"),
56
- Check("moe.route", (1, 0), {"input": {"query": "conformance test"}}, "1.0", ["candidates"], description="moe.route returns candidates"),
57
-
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  # Model distribution
59
- Check("model.list", (1, 0), {"input": {}}, "1.0", ["models"], description="model.list returns models"),
60
-
 
 
 
 
 
 
61
  # Tool: plant (validates input handling)
62
- Check("tool.plant_identify", (1, 0), {"input": {}}, "1.0", [], expect_error="bad_request", description="tool.plant_identify rejects missing image"),
63
-
 
 
 
 
 
 
 
64
  # Phase 2 (suite 2.0) β€” only if registered
65
- Check("ocr.image", (1, 0), {"input": {"image_cid": "blake3:00000000"}}, "2.0", [], description="ocr.image endpoint exists"),
66
- Check("trans.text", (1, 0), {"input": {"text": "hello", "from": "en", "to": "de"}}, "2.0", [], description="trans.text responds"),
67
- Check("rerank.text", (1, 0), {"input": {"query": "test", "documents": [{"id": "d1", "text": "test"}]}}, "2.0", [], description="rerank.text responds"),
68
- Check("img.describe", (1, 0), {"input": {"image_cid": "blake3:00000000", "task": "caption"}}, "2.0", [], description="img.describe responds"),
69
- Check("stt.transcribe", (1, 0), {"input": {"audio_cid": "blake3:00000000"}}, "2.0", [], description="stt.transcribe responds"),
70
- Check("tts.synthesize", (1, 0), {"input": {"text": "ping", "speed": 1.0, "format": "wav"}}, "2.0", [], description="tts.synthesize responds"),
71
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  # Phase 3 experimental (suite 3.0)
73
- Check("moe.register", (1, 0), {"input": {"expert_id": "model:x09", "expert_type": "model", "topic_tags": ["x09"], "confidence_score": 0.5, "community_id": "x09"}}, "3.0", ["registered"], description="moe.register accepts expert"),
74
- Check("model.status", (1, 0), {"input": {}}, "3.0", ["jobs"], description="model.status returns jobs"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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="" if passed else f"expected_error={check.expect_error}, got={error_code}",
 
 
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 # 1 MiB
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(self._handle_change(zeroconf, service_type, name, state_change))
 
 
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
- from contextlib import suppress
136
- with suppress(Exception):
137
- await self._zeroconf.async_close()
 
 
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
- from contextlib import suppress
145
- for q in list(self._subscribers):
146
- with suppress(asyncio.QueueFull):
147
- q.put_nowait(event)
 
 
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 len(self._transition_times) >= EMERGENCY_ANTI_FLAP_MAX_TRANSITIONS and old_mode in ("degraded", "offline") and new_mode == "online":
 
 
 
 
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 datetime, timezone
20
  from pathlib import Path
21
 
22
- UTC = timezone.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 (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,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 (view, ft) in self._views.values():
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 datetime, timezone
9
  from pathlib import Path
10
  from typing import TYPE_CHECKING, Any
11
 
12
- UTC = timezone.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 (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]
 
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 datetime, timedelta, timezone
5
  from typing import Any
6
 
7
- UTC = timezone.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 Ò†’ 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"
 
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
- "latency_ms": latency_ms,
300
- "tokens_in": tokens_in,
301
- "tokens_out": tokens_out,
302
- "model": model,
303
- "backend": backend,
304
- "result": result,
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
- "mesh_size": mesh_size,
313
- "online": int(online),
314
- "capability_count": cap_count,
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 opentelemetry import metrics as otel_metrics # type: ignore[import]
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 = True
 
 
 
 
 
 
 
 
 
 
 
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 datetime, timezone
5
 
6
  from hearthnet.bus.capability import CapabilityDescriptor, RouteRequest
7
 
8
- UTC = timezone.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, members in self._members.items():
277
- if member_id in members:
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(members),
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 = 0.0
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
- "inputs": prompt,
72
- "parameters": {"max_new_tokens": max_tokens, "return_full_text": False},
73
- }).encode()
 
 
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
- import llama_cpp
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
- (endpoint or os.getenv("MODAL_ENDPOINT", "")).rstrip("/")
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 httpx.AsyncClient(timeout=120.0) as client, client.stream("POST", f"{self._base_url}/api/chat", json=payload) as resp:
 
 
 
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 httpx.AsyncClient(timeout=60.0) as client, client.stream(
110
- "POST",
111
- f"{self._base_url}/chat/completions",
112
- json=payload,
113
- headers=headers,
114
- ) as resp:
 
 
 
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 datetime, timezone
5
  from typing import Literal
6
 
7
- UTC = timezone.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 datetime, timedelta, timezone
5
 
6
  from hearthnet.bus.capability import CapabilityDescriptor, RouteRequest
7
 
8
- UTC = timezone.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 datetime, timezone
4
  from typing import Any
5
 
6
- UTC = timezone.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 β€” "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", "")
 
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
- ("market.post", (1, 0), {"input": {"title": "__conformance__", "body": "test", "category": "other", "client_id": "__x09__"}}, ""),
47
- ("chat.send", (1, 0), {"input": {"to": "self", "body": "ping", "client_id": "__x09_chat__"}}, ""),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- ("rerank.text", (1, 0), {"input": {"query": "test", "documents": [{"id": "d1", "text": "test"}]}}, ""),
 
 
 
 
 
59
  ]
60
 
61
  _SUITE_V3: list[tuple[str, tuple[int, int], dict, str]] = [
62
  # Phase 3 experimental
63
- ("moe.register", (1, 0), {"input": {"expert_id": "model:x09", "expert_type": "model", "topic_tags": ["test"], "confidence_score": 0.5, "community_id": "test"}}, "registered"),
 
 
 
 
 
 
 
 
 
 
 
 
 
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({"capability": cap_name, "passed": False, "skipped": True, "error": "no_bus"})
 
 
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({"capability": cap_name, "passed": False, "skipped": True, "error": "not_registered"})
 
 
 
 
 
 
 
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 = "error" in result and result["error"] not in (
210
- "bad_request", # some capabilities intentionally return bad_request for empty input
211
- None,
 
 
 
 
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({"capability": cap_name, "passed": False, "skipped": False, "error": str(error_msg)})
 
 
 
 
 
 
 
222
  failed += 1
223
  else:
224
- results.append({"capability": cap_name, "passed": True, "skipped": False, "error": ""})
 
 
225
  passed += 1
226
  except Exception as exc:
227
- results.append({"capability": cap_name, "passed": False, "skipped": False, "error": str(exc)})
 
 
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 # local-first threshold (C)
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
- format: str = "ogg_vorbis",
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
- format: str = "ogg_vorbis",
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 = (f"\nAdditional context: {', '.join(hints)}" if hints else "")
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 datetime, timezone
11
 
12
- UTC = timezone.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 httpx.AsyncClient(verify=self._verify_ssl) as client, client.stream(
114
- "POST",
115
- f"{self._base_url}/bus/v1/call",
116
- json=payload,
117
- headers=headers,
118
- ) as resp:
 
 
 
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 datetime, timezone
25
  from typing import Any
26
 
27
- UTC = timezone.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) # "<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
 
 
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