aethergraph 0.1.0a2__py3-none-any.whl → 0.1.0a4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. aethergraph/__main__.py +3 -0
  2. aethergraph/api/v1/artifacts.py +23 -4
  3. aethergraph/api/v1/schemas.py +7 -0
  4. aethergraph/api/v1/session.py +123 -4
  5. aethergraph/config/config.py +2 -0
  6. aethergraph/config/search.py +49 -0
  7. aethergraph/contracts/services/channel.py +18 -1
  8. aethergraph/contracts/services/execution.py +58 -0
  9. aethergraph/contracts/services/llm.py +26 -0
  10. aethergraph/contracts/services/memory.py +10 -4
  11. aethergraph/contracts/services/planning.py +53 -0
  12. aethergraph/contracts/storage/event_log.py +8 -0
  13. aethergraph/contracts/storage/search_backend.py +47 -0
  14. aethergraph/contracts/storage/vector_index.py +73 -0
  15. aethergraph/core/graph/action_spec.py +76 -0
  16. aethergraph/core/graph/graph_fn.py +75 -2
  17. aethergraph/core/graph/graphify.py +74 -2
  18. aethergraph/core/runtime/graph_runner.py +2 -1
  19. aethergraph/core/runtime/node_context.py +66 -3
  20. aethergraph/core/runtime/node_services.py +8 -0
  21. aethergraph/core/runtime/run_manager.py +263 -271
  22. aethergraph/core/runtime/run_types.py +54 -1
  23. aethergraph/core/runtime/runtime_env.py +35 -14
  24. aethergraph/core/runtime/runtime_services.py +308 -18
  25. aethergraph/plugins/agents/default_chat_agent.py +266 -74
  26. aethergraph/plugins/agents/default_chat_agent_v2.py +487 -0
  27. aethergraph/plugins/channel/adapters/webui.py +69 -21
  28. aethergraph/plugins/channel/routes/webui_routes.py +8 -48
  29. aethergraph/runtime/__init__.py +12 -0
  30. aethergraph/server/app_factory.py +10 -1
  31. aethergraph/server/ui_static/assets/index-CFktGdbW.js +4913 -0
  32. aethergraph/server/ui_static/assets/index-DcfkFlTA.css +1 -0
  33. aethergraph/server/ui_static/index.html +2 -2
  34. aethergraph/services/artifacts/facade.py +157 -21
  35. aethergraph/services/artifacts/types.py +35 -0
  36. aethergraph/services/artifacts/utils.py +42 -0
  37. aethergraph/services/channel/channel_bus.py +3 -1
  38. aethergraph/services/channel/event_hub copy.py +55 -0
  39. aethergraph/services/channel/event_hub.py +81 -0
  40. aethergraph/services/channel/factory.py +3 -2
  41. aethergraph/services/channel/session.py +709 -74
  42. aethergraph/services/container/default_container.py +69 -7
  43. aethergraph/services/execution/__init__.py +0 -0
  44. aethergraph/services/execution/local_python.py +118 -0
  45. aethergraph/services/indices/__init__.py +0 -0
  46. aethergraph/services/indices/global_indices.py +21 -0
  47. aethergraph/services/indices/scoped_indices.py +292 -0
  48. aethergraph/services/llm/generic_client.py +342 -46
  49. aethergraph/services/llm/generic_embed_client.py +359 -0
  50. aethergraph/services/llm/types.py +3 -1
  51. aethergraph/services/memory/distillers/llm_long_term.py +60 -109
  52. aethergraph/services/memory/distillers/llm_long_term_v1.py +180 -0
  53. aethergraph/services/memory/distillers/llm_meta_summary.py +57 -266
  54. aethergraph/services/memory/distillers/llm_meta_summary_v1.py +342 -0
  55. aethergraph/services/memory/distillers/long_term.py +48 -131
  56. aethergraph/services/memory/distillers/long_term_v1.py +170 -0
  57. aethergraph/services/memory/facade/chat.py +18 -8
  58. aethergraph/services/memory/facade/core.py +159 -19
  59. aethergraph/services/memory/facade/distillation.py +86 -31
  60. aethergraph/services/memory/facade/retrieval.py +100 -1
  61. aethergraph/services/memory/factory.py +4 -1
  62. aethergraph/services/planning/__init__.py +0 -0
  63. aethergraph/services/planning/action_catalog.py +271 -0
  64. aethergraph/services/planning/bindings.py +56 -0
  65. aethergraph/services/planning/dependency_index.py +65 -0
  66. aethergraph/services/planning/flow_validator.py +263 -0
  67. aethergraph/services/planning/graph_io_adapter.py +150 -0
  68. aethergraph/services/planning/input_parser.py +312 -0
  69. aethergraph/services/planning/missing_inputs.py +28 -0
  70. aethergraph/services/planning/node_planner.py +613 -0
  71. aethergraph/services/planning/orchestrator.py +112 -0
  72. aethergraph/services/planning/plan_executor.py +506 -0
  73. aethergraph/services/planning/plan_types.py +321 -0
  74. aethergraph/services/planning/planner.py +617 -0
  75. aethergraph/services/planning/planner_service.py +369 -0
  76. aethergraph/services/planning/planning_context_builder.py +43 -0
  77. aethergraph/services/planning/quick_actions.py +29 -0
  78. aethergraph/services/planning/routers/__init__.py +0 -0
  79. aethergraph/services/planning/routers/simple_router.py +26 -0
  80. aethergraph/services/rag/facade.py +0 -3
  81. aethergraph/services/scope/scope.py +30 -30
  82. aethergraph/services/scope/scope_factory.py +15 -7
  83. aethergraph/services/skills/__init__.py +0 -0
  84. aethergraph/services/skills/skill_registry.py +465 -0
  85. aethergraph/services/skills/skills.py +220 -0
  86. aethergraph/services/skills/utils.py +194 -0
  87. aethergraph/storage/artifacts/artifact_index_jsonl.py +16 -10
  88. aethergraph/storage/artifacts/artifact_index_sqlite.py +12 -2
  89. aethergraph/storage/docstore/sqlite_doc_sync.py +1 -1
  90. aethergraph/storage/memory/event_persist.py +42 -2
  91. aethergraph/storage/memory/fs_persist.py +32 -2
  92. aethergraph/storage/search_backend/__init__.py +0 -0
  93. aethergraph/storage/search_backend/generic_vector_backend.py +230 -0
  94. aethergraph/storage/search_backend/null_backend.py +34 -0
  95. aethergraph/storage/search_backend/sqlite_lexical_backend.py +387 -0
  96. aethergraph/storage/search_backend/utils.py +31 -0
  97. aethergraph/storage/search_factory.py +75 -0
  98. aethergraph/storage/vector_index/faiss_index.py +72 -4
  99. aethergraph/storage/vector_index/sqlite_index.py +521 -52
  100. aethergraph/storage/vector_index/sqlite_index_vanila.py +311 -0
  101. aethergraph/storage/vector_index/utils.py +22 -0
  102. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/METADATA +1 -1
  103. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/RECORD +108 -64
  104. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/WHEEL +1 -1
  105. aethergraph/plugins/agents/default_chat_agent copy.py +0 -90
  106. aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +0 -1
  107. aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +0 -400
  108. aethergraph/services/eventhub/event_hub.py +0 -76
  109. aethergraph/services/llm/generic_client copy.py +0 -691
  110. aethergraph/services/prompts/file_store.py +0 -41
  111. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/entry_points.txt +0 -0
  112. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/LICENSE +0 -0
  113. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/NOTICE +0 -0
  114. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,7 @@
1
- from dataclasses import dataclass, field
1
+ from dataclasses import asdict, dataclass, field, is_dataclass
2
2
  from datetime import datetime
3
3
  from enum import Enum
4
+ import json
4
5
  from typing import Any
5
6
 
6
7
  # used to represent the status of a run, primiarily used in endpoint with RunManager
@@ -87,3 +88,55 @@ class SessionKind(str, Enum):
87
88
  playground = "playground"
88
89
  notebook = "notebook"
89
90
  pipline = "pipeline" # future
91
+
92
+
93
+ MAX_PREVIEW_CHARS = 16_000 # tune as you like
94
+
95
+
96
+ def _to_json_ish(obj: Any) -> Any:
97
+ """
98
+ Best-effort conversion of arbitrary Python objects to something JSON-ish.
99
+ This is ONLY for UI preview, never for planner semantics.
100
+ """
101
+ if obj is None:
102
+ return None
103
+ if isinstance(obj, (str, int, float, bool)): # noqa: UP038
104
+ return obj
105
+ if isinstance(obj, (list, tuple, set)): # noqa: UP038
106
+ return [_to_json_ish(x) for x in obj]
107
+ if isinstance(obj, dict):
108
+ return {str(k): _to_json_ish(v) for k, v in obj.items()}
109
+ if is_dataclass(obj):
110
+ return _to_json_ish(asdict(obj))
111
+
112
+ # Optional: numpy handling
113
+ try:
114
+ import numpy as np # type: ignore
115
+
116
+ if isinstance(obj, np.ndarray):
117
+ return {
118
+ "__ndarray__": True,
119
+ "shape": obj.shape,
120
+ "dtype": str(obj.dtype),
121
+ "preview": _to_json_ish(obj.flatten()[:10].tolist()),
122
+ }
123
+ except Exception:
124
+ pass
125
+
126
+ return repr(obj)
127
+
128
+
129
+ def _make_preview(obj: Any, max_chars: int = MAX_PREVIEW_CHARS) -> tuple[str, bool]:
130
+ """
131
+ Returns (preview_str, truncated_flag).
132
+ Never used for planning; only for UI.
133
+ """
134
+ jsonish = _to_json_ish(obj)
135
+ try:
136
+ s = json.dumps(jsonish, ensure_ascii=False)
137
+ except TypeError:
138
+ s = repr(jsonish)
139
+ truncated = len(s) > max_chars
140
+ if truncated:
141
+ s = s[:max_chars] + "... (truncated)"
142
+ return s, truncated
@@ -17,6 +17,7 @@ from aethergraph.services.continuations.stores.fs_store import (
17
17
  )
18
18
 
19
19
  # ---- memory services ----
20
+ from aethergraph.services.indices.scoped_indices import ScopedIndices
20
21
  from aethergraph.services.memory.facade import MemoryFacade
21
22
  from aethergraph.services.rag.node_rag import NodeRAG
22
23
  from aethergraph.services.resume.router import ResumeRouter
@@ -118,6 +119,18 @@ class RuntimeEnv:
118
119
  "entities": [],
119
120
  }
120
121
 
122
+ node_scope = (
123
+ self.container.scope_factory.for_node(
124
+ identity=self.identity,
125
+ run_id=self.run_id,
126
+ graph_id=self.graph_id,
127
+ node_id=node.node_id,
128
+ session_id=self.session_id,
129
+ )
130
+ if self.container.scope_factory
131
+ else None
132
+ )
133
+
121
134
  level, custom_scope_id = self._resolve_memory_config()
122
135
  mem_scope = (
123
136
  self.container.scope_factory.for_memory(
@@ -133,24 +146,25 @@ class RuntimeEnv:
133
146
  else None
134
147
  )
135
148
 
149
+ indices: ScopedIndices | None = None # scoped indices for this node
150
+ if self.container.global_indices is not None and node_scope is not None:
151
+ # Attach scoped indices to container for this node's scope
152
+ # Prefer memory scope id if available for memory-tied corpora
153
+ base_scope = mem_scope or node_scope
154
+ if base_scope:
155
+ scope_id = mem_scope.memory_scope_id() if mem_scope else None
156
+ indices = self.container.global_indices.for_scope(
157
+ scope=base_scope,
158
+ scope_id=scope_id,
159
+ )
160
+
136
161
  mem: MemoryFacade = self.memory_factory.for_session(
137
162
  run_id=self.run_id,
138
163
  graph_id=self.graph_id,
139
164
  node_id=node.node_id,
140
165
  session_id=self.session_id,
141
166
  scope=mem_scope,
142
- )
143
-
144
- node_scope = (
145
- self.container.scope_factory.for_node(
146
- identity=self.identity,
147
- run_id=self.run_id,
148
- graph_id=self.graph_id,
149
- node_id=node.node_id,
150
- session_id=self.session_id,
151
- )
152
- if self.container.scope_factory
153
- else None
167
+ scoped_indices=indices,
154
168
  )
155
169
 
156
170
  from aethergraph.services.artifacts.facade import ArtifactFacade
@@ -161,8 +175,9 @@ class RuntimeEnv:
161
175
  node_id=node.node_id,
162
176
  tool_name=node.tool_name,
163
177
  tool_version=node.tool_version, # to be filled from node if available
164
- store=self.artifacts,
165
- index=self.artifact_index,
178
+ art_store=self.artifacts,
179
+ art_index=self.artifact_index,
180
+ scoped_indices=indices,
166
181
  scope=node_scope,
167
182
  )
168
183
 
@@ -202,6 +217,12 @@ class RuntimeEnv:
202
217
  rag=rag_for_node, # RAGService
203
218
  mcp=self.mcp_service, # MCPService
204
219
  run_manager=self.container.run_manager, # RunManager
220
+ indices=indices, # ScopedIndices for this node
221
+ execution=self.container.execution
222
+ if self.container.execution is not None
223
+ else None, # ExecutionService
224
+ planner_service=self.container.planner_service,
225
+ skills=self.container.skills_registry,
205
226
  )
206
227
  return ExecutionContext(
207
228
  run_id=self.run_id,
@@ -2,11 +2,14 @@ from collections.abc import Callable
2
2
  from contextlib import contextmanager
3
3
  from contextvars import ContextVar
4
4
  from pathlib import Path
5
+ from threading import RLock
5
6
  from typing import Any
6
7
 
7
8
  from aethergraph.contracts.services.llm import LLMClientProtocol
8
9
  from aethergraph.core.runtime.base_service import Service
9
10
  from aethergraph.services.llm.generic_client import GenericLLMClient
11
+ from aethergraph.services.skills.skill_registry import SkillRegistry
12
+ from aethergraph.services.skills.skills import Skill
10
13
 
11
14
  _current = ContextVar("aeg_services", default=None)
12
15
  # process-wide fallback (handles contextvar boundary issues)
@@ -15,18 +18,72 @@ _services_global: Any = None
15
18
  _pending_ext_services: dict[str, Any] = {}
16
19
 
17
20
 
21
+ _pending_lock = RLock()
22
+
23
+ # Ordered operations (some things depend on earlier steps)
24
+ _pending_ops_order: list[str] = []
25
+ # Keyed storage so repeated registrations overwrite instead of duplicating
26
+ _pending_ops: dict[str, Callable[[Any], Any]] = {}
27
+ # Optional: store results if you want “handles” later
28
+ _pending_results: dict[str, Any] = {}
29
+
30
+
31
+ def _defer_op(key: str, op: Callable[[Any], Any]) -> None:
32
+ """Register (or replace) a deferred operation."""
33
+ with _pending_lock:
34
+ if key not in _pending_ops:
35
+ _pending_ops_order.append(key)
36
+ _pending_ops[key] = op
37
+
38
+
39
+ def _flush_pending_ops(services: Any) -> None:
40
+ """Apply all deferred operations once services exist."""
41
+ with _pending_lock:
42
+ keys = list(_pending_ops_order)
43
+ _pending_ops_order.clear()
44
+ ops = _pending_ops.copy()
45
+ _pending_ops.clear()
46
+
47
+ for key in keys:
48
+ op = ops.get(key)
49
+ if op is None:
50
+ continue
51
+ try:
52
+ _pending_results[key] = op(services)
53
+ except Exception:
54
+ # You can choose to log here instead of raising,
55
+ # but raising is usually better so startup fails loudly.
56
+ raise
57
+
58
+
59
+ def _try_apply_or_defer(key: str, fn: Callable[[Any], Any]) -> Any | None:
60
+ """
61
+ If services installed: run now and return result.
62
+ Else: defer it and return None.
63
+ """
64
+ try:
65
+ svc = current_services()
66
+ except RuntimeError:
67
+ _defer_op(key, fn)
68
+ return None
69
+ else:
70
+ return fn(svc)
71
+
72
+
18
73
  def install_services(services: Any) -> None:
19
74
  global _services_global, _pending_ext_services
20
75
  _services_global = services
21
76
 
22
- # Attach any services that were registered before install_services().
77
+ # Attach pending ext services (your existing behavior)
23
78
  ext = getattr(services, "ext_services", None)
24
79
  if isinstance(ext, dict) and _pending_ext_services:
25
- # Don't clobber anything that was already present.
26
80
  for name, svc in _pending_ext_services.items():
27
81
  ext.setdefault(name, svc)
28
82
  _pending_ext_services = {}
29
83
 
84
+ # NEW: apply all other pending mutations
85
+ _flush_pending_ops(services)
86
+
30
87
  return _current.set(services)
31
88
 
32
89
 
@@ -37,12 +94,16 @@ def ensure_services_installed(factory: Callable[[], Any]) -> Any:
37
94
  svc = factory()
38
95
  _services_global = svc
39
96
 
40
- # hydrate pending external services here too
97
+ # hydrate pending external services
41
98
  ext = getattr(svc, "ext_services", None)
42
99
  if isinstance(ext, dict) and _pending_ext_services:
43
100
  for name, s in _pending_ext_services.items():
44
101
  ext.setdefault(name, s)
45
102
  _pending_ext_services = {}
103
+
104
+ # NEW: apply pending ops on first creation too
105
+ _flush_pending_ops(svc)
106
+
46
107
  _current.set(svc)
47
108
  return svc
48
109
 
@@ -72,9 +133,10 @@ def get_channel_service() -> Any:
72
133
 
73
134
 
74
135
  def set_default_channel(key: str) -> None:
75
- svc = current_services()
76
- svc.channels.set_default_channel_key(key)
77
- return
136
+ def _op(svc: Any) -> None:
137
+ svc.channels.set_default_channel_key(key)
138
+
139
+ return _try_apply_or_defer(key, _op)
78
140
 
79
141
 
80
142
  def get_default_channel() -> str:
@@ -89,7 +151,7 @@ def set_channel_alias(alias: str, channel_key: str) -> None:
89
151
 
90
152
  def register_channel_adapter(name: str, adapter: Any) -> None:
91
153
  svc = current_services()
92
- svc.channel.register_adapter(name, adapter)
154
+ svc.channels.register_adapter(name, adapter)
93
155
 
94
156
 
95
157
  # --------- LLM service helpers ---------
@@ -107,17 +169,20 @@ def register_llm_client(
107
169
  api_key: str | None = None,
108
170
  timeout: float | None = None,
109
171
  ) -> None:
110
- svc = current_services()
111
- client = svc.llm.configure_profile(
112
- profile=profile,
113
- provider=provider,
114
- model=model,
115
- embed_model=embed_model,
116
- base_url=base_url,
117
- api_key=api_key,
118
- timeout=timeout,
119
- )
120
- return client
172
+ def _op(svc: Any) -> LLMClientProtocol:
173
+ client = svc.llm.configure_profile(
174
+ profile=profile,
175
+ provider=provider,
176
+ model=model,
177
+ embed_model=embed_model,
178
+ base_url=base_url,
179
+ api_key=api_key,
180
+ timeout=timeout,
181
+ )
182
+ return client
183
+
184
+ key = f"llm_client:profile={profile}:provider={provider}:model={model}"
185
+ return _try_apply_or_defer(key, _op)
121
186
 
122
187
 
123
188
  # backend compatibility
@@ -402,6 +467,231 @@ def list_mcp_clients() -> list[str]:
402
467
  return []
403
468
 
404
469
 
470
+ # --------- Skill registry helpers ---------
471
+ def get_skill_registry() -> SkillRegistry:
472
+ svc = current_services()
473
+ return svc.skills_registry
474
+
475
+
476
+ def register_skill(skill: Skill, *, overwrite: bool = False) -> Skill:
477
+ """
478
+ Register an existing Skill object into the global registry.
479
+
480
+ This method adds a `Skill` instance to the global `SkillRegistry`, making it
481
+ available for use throughout the application. The `overwrite` flag determines
482
+ whether an existing skill with the same ID will be replaced.
483
+
484
+ Examples:
485
+ Registering a skill object:
486
+ ```python
487
+ skill = Skill(id="example.skill", title="Example Skill")
488
+ register_skill(skill)
489
+ ```
490
+
491
+ Overwriting an existing skill:
492
+ ```python
493
+ skill = Skill(id="example.skill", title="Updated Skill")
494
+ register_skill(skill, overwrite=True)
495
+ ```
496
+
497
+ Args:
498
+ skill: The `Skill` object to register.
499
+ overwrite: Whether to overwrite an existing skill with the same ID. Default is `False`.
500
+
501
+ Returns:
502
+ Skill: The registered `Skill` instance.
503
+
504
+ """
505
+
506
+ def _op(svc: Any) -> "Skill":
507
+ reg = svc.skills_registry
508
+ reg.register(skill, overwrite=overwrite)
509
+ return skill
510
+
511
+ # Key should be stable and allow overwriting the deferred op if called again.
512
+ # Usually skill.id is the right identity here.
513
+ key = f"skills:obj:{skill.id}:overwrite={overwrite}"
514
+ return _try_apply_or_defer(key, _op)
515
+
516
+
517
+ def register_skill_inline(
518
+ *,
519
+ id: str,
520
+ title: str,
521
+ description: str = "",
522
+ tags: list[str] | None = None,
523
+ domain: str | None = None,
524
+ modes: list[str] | None = None,
525
+ version: str | None = None,
526
+ config: dict[str, Any] | None = None,
527
+ sections: dict[str, str] | None = None,
528
+ overwrite: bool = False,
529
+ ) -> Skill:
530
+ """
531
+ Define and register a Skill entirely in Python.
532
+
533
+ This method allows you to define a Skill inline with all its metadata and sections,
534
+ and directly register it into the global Skill registry.
535
+
536
+ Examples:
537
+ Registering a skill with basic metadata and sections:
538
+ ```python
539
+ register_skill_inline(
540
+ id="surrogate.workflow",
541
+ title="Surrogate workflow planning",
542
+ description="Prompts and patterns for surrogate planning.",
543
+ tags=["surrogate", "planning"],
544
+ modes=["planning"],
545
+ sections={
546
+ "planning.header": "...",
547
+ "planning.binding_hints": "...",
548
+ "chat.system": "...",
549
+ },
550
+ )
551
+ ```
552
+
553
+ Args:
554
+ id (str): The unique identifier for the Skill. (Required)
555
+ title (str): A human-readable title for the Skill. (Required)
556
+ description (str): A short description of the Skill's purpose. (Optional)
557
+ tags (list[str]): A list of tags for categorization. (Optional)
558
+ domain (str): The domain or namespace for the Skill. (Optional)
559
+ modes (list[str]): The operational modes supported by the Skill. (Optional)
560
+ version (str): The version string for the Skill. (Optional)
561
+ config (dict[str, Any]): Additional configuration data. (Optional)
562
+ sections (dict[str, str]): A dictionary mapping section names to their content. (Optional)
563
+ overwrite (bool): Whether to overwrite an existing Skill with the same ID. (Optional)
564
+
565
+ Returns:
566
+ Skill: The registered Skill instance.
567
+ """
568
+
569
+ def _op(svc: Any) -> "Skill":
570
+ reg = svc.skills_registry
571
+ return reg.register_inline(
572
+ id=id,
573
+ title=title,
574
+ description=description,
575
+ tags=tags,
576
+ domain=domain,
577
+ modes=modes,
578
+ version=version,
579
+ config=config,
580
+ sections=sections,
581
+ overwrite=overwrite,
582
+ )
583
+
584
+ # Include overwrite, and optionally version to avoid surprising replacements.
585
+ key = f"skills:inline:{id}:overwrite={overwrite}:version={version or ''}"
586
+ return _try_apply_or_defer(key, _op)
587
+
588
+
589
+ def register_skill_file(path: str | Path, *, overwrite: bool = False) -> Skill:
590
+ """
591
+ Load a single markdown skill file and register it.
592
+
593
+ This function processes a markdown file containing skill definitions and
594
+ registers it into the global skill registry. The file must adhere to the
595
+ expected format for parsing skill metadata and sections.
596
+
597
+ Examples:
598
+ Registering a skill from a markdown file:
599
+ ```python
600
+ skill = register_skill_file("skills/surrogate-workflow.md")
601
+ ```
602
+
603
+ Args:
604
+ path: The path to the markdown file to load.
605
+ overwrite: Whether to overwrite an existing skill with the same ID. (Optional, default: False)
606
+
607
+ Returns:
608
+ Skill: The registered `Skill` instance.
609
+
610
+ Notes:
611
+ To start the server and load all desired packages:
612
+ 1. Open a terminal and navigate to the project directory.
613
+ 2. Run the server using the appropriate command (e.g., `python -m aethergraph.server`).
614
+ 3. Ensure all required dependencies are installed via `pip install -r requirements.txt`.
615
+
616
+ """
617
+
618
+ p = str(path)
619
+
620
+ def _op(svc: Any) -> Skill:
621
+ reg = svc.skills_registry
622
+ return reg.load_file(path, overwrite=overwrite)
623
+
624
+ p = str(path)
625
+
626
+ def _op(svc: Any) -> "Skill":
627
+ reg = svc.skills_registry
628
+ return reg.load_file(path, overwrite=overwrite)
629
+
630
+ key = f"skills:file:{p}:overwrite={overwrite}"
631
+ return _try_apply_or_defer(key, _op)
632
+
633
+
634
+ def register_skills_from_path(
635
+ root: str | Path,
636
+ *,
637
+ pattern: str = "*.md",
638
+ recursive: bool = True,
639
+ overwrite: bool = False,
640
+ ) -> list[Skill]:
641
+ """
642
+ Load and register all skill markdown files under a directory.
643
+
644
+ This method scans the specified directory for markdown files matching the
645
+ given pattern, parses their content into `Skill` objects, and registers
646
+ them into the global skill registry. The directory can have a flat or
647
+ nested structure.
648
+
649
+ Examples:
650
+ Register all skills in a flat directory:
651
+ ```python
652
+ register_skills_from_path("skills/")
653
+ ```
654
+
655
+ Register skills in a nested directory structure:
656
+ ```python
657
+ register_skills_from_path("skills/", recursive=True)
658
+ ```
659
+
660
+ Use a custom file pattern to filter files:
661
+ ```python
662
+ register_skills_from_path("skills/", pattern="*.skill.md")
663
+ ```
664
+
665
+ Args:
666
+ root: The root directory to scan for skill files.
667
+ pattern: A glob pattern to match skill files. Default is `"*.md"`.
668
+ recursive: Whether to scan subdirectories recursively. Default is `True`.
669
+ overwrite: Whether to overwrite existing skills with the same ID. Default is `False`.
670
+
671
+ Returns:
672
+ list[Skill]: A list of all registered `Skill` objects.
673
+
674
+ Notes:
675
+ To start the server and load all desired packages:
676
+ 1. Open a terminal and navigate to the project directory.
677
+ 2. Run the server using the appropriate command (e.g., `python -m aethergraph.server`).
678
+ 3. Ensure all required dependencies are installed via `pip install -r requirements.txt`.
679
+
680
+ """
681
+ root_str = str(root)
682
+
683
+ def _op(svc: Any) -> list[Skill]:
684
+ return svc.skills_registry.load_path(
685
+ root=root_str,
686
+ pattern=pattern,
687
+ recursive=recursive,
688
+ overwrite=overwrite,
689
+ )
690
+
691
+ key = f"skills:path:{root_str}:pattern={pattern}:recursive={recursive}:overwrite={overwrite}"
692
+ return _try_apply_or_defer(key, _op)
693
+
694
+
405
695
  # --------- Scheduler helpers --------- - (Not used)
406
696
  def ensure_global_scheduler_started() -> None:
407
697
  svc = current_services()