aethergraph 0.1.0a3__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 (113) hide show
  1. aethergraph/api/v1/artifacts.py +23 -4
  2. aethergraph/api/v1/schemas.py +7 -0
  3. aethergraph/api/v1/session.py +123 -4
  4. aethergraph/config/config.py +2 -0
  5. aethergraph/config/search.py +49 -0
  6. aethergraph/contracts/services/channel.py +18 -1
  7. aethergraph/contracts/services/execution.py +58 -0
  8. aethergraph/contracts/services/llm.py +26 -0
  9. aethergraph/contracts/services/memory.py +10 -4
  10. aethergraph/contracts/services/planning.py +53 -0
  11. aethergraph/contracts/storage/event_log.py +8 -0
  12. aethergraph/contracts/storage/search_backend.py +47 -0
  13. aethergraph/contracts/storage/vector_index.py +73 -0
  14. aethergraph/core/graph/action_spec.py +76 -0
  15. aethergraph/core/graph/graph_fn.py +75 -2
  16. aethergraph/core/graph/graphify.py +74 -2
  17. aethergraph/core/runtime/graph_runner.py +2 -1
  18. aethergraph/core/runtime/node_context.py +66 -3
  19. aethergraph/core/runtime/node_services.py +8 -0
  20. aethergraph/core/runtime/run_manager.py +263 -271
  21. aethergraph/core/runtime/run_types.py +54 -1
  22. aethergraph/core/runtime/runtime_env.py +35 -14
  23. aethergraph/core/runtime/runtime_services.py +308 -18
  24. aethergraph/plugins/agents/default_chat_agent.py +266 -74
  25. aethergraph/plugins/agents/default_chat_agent_v2.py +487 -0
  26. aethergraph/plugins/channel/adapters/webui.py +69 -21
  27. aethergraph/plugins/channel/routes/webui_routes.py +8 -48
  28. aethergraph/runtime/__init__.py +12 -0
  29. aethergraph/server/app_factory.py +3 -0
  30. aethergraph/server/ui_static/assets/index-CFktGdbW.js +4913 -0
  31. aethergraph/server/ui_static/assets/index-DcfkFlTA.css +1 -0
  32. aethergraph/server/ui_static/index.html +2 -2
  33. aethergraph/services/artifacts/facade.py +157 -21
  34. aethergraph/services/artifacts/types.py +35 -0
  35. aethergraph/services/artifacts/utils.py +42 -0
  36. aethergraph/services/channel/channel_bus.py +3 -1
  37. aethergraph/services/channel/event_hub copy.py +55 -0
  38. aethergraph/services/channel/event_hub.py +81 -0
  39. aethergraph/services/channel/factory.py +3 -2
  40. aethergraph/services/channel/session.py +709 -74
  41. aethergraph/services/container/default_container.py +69 -7
  42. aethergraph/services/execution/__init__.py +0 -0
  43. aethergraph/services/execution/local_python.py +118 -0
  44. aethergraph/services/indices/__init__.py +0 -0
  45. aethergraph/services/indices/global_indices.py +21 -0
  46. aethergraph/services/indices/scoped_indices.py +292 -0
  47. aethergraph/services/llm/generic_client.py +342 -46
  48. aethergraph/services/llm/generic_embed_client.py +359 -0
  49. aethergraph/services/llm/types.py +3 -1
  50. aethergraph/services/memory/distillers/llm_long_term.py +60 -109
  51. aethergraph/services/memory/distillers/llm_long_term_v1.py +180 -0
  52. aethergraph/services/memory/distillers/llm_meta_summary.py +57 -266
  53. aethergraph/services/memory/distillers/llm_meta_summary_v1.py +342 -0
  54. aethergraph/services/memory/distillers/long_term.py +48 -131
  55. aethergraph/services/memory/distillers/long_term_v1.py +170 -0
  56. aethergraph/services/memory/facade/chat.py +18 -8
  57. aethergraph/services/memory/facade/core.py +159 -19
  58. aethergraph/services/memory/facade/distillation.py +86 -31
  59. aethergraph/services/memory/facade/retrieval.py +100 -1
  60. aethergraph/services/memory/factory.py +4 -1
  61. aethergraph/services/planning/__init__.py +0 -0
  62. aethergraph/services/planning/action_catalog.py +271 -0
  63. aethergraph/services/planning/bindings.py +56 -0
  64. aethergraph/services/planning/dependency_index.py +65 -0
  65. aethergraph/services/planning/flow_validator.py +263 -0
  66. aethergraph/services/planning/graph_io_adapter.py +150 -0
  67. aethergraph/services/planning/input_parser.py +312 -0
  68. aethergraph/services/planning/missing_inputs.py +28 -0
  69. aethergraph/services/planning/node_planner.py +613 -0
  70. aethergraph/services/planning/orchestrator.py +112 -0
  71. aethergraph/services/planning/plan_executor.py +506 -0
  72. aethergraph/services/planning/plan_types.py +321 -0
  73. aethergraph/services/planning/planner.py +617 -0
  74. aethergraph/services/planning/planner_service.py +369 -0
  75. aethergraph/services/planning/planning_context_builder.py +43 -0
  76. aethergraph/services/planning/quick_actions.py +29 -0
  77. aethergraph/services/planning/routers/__init__.py +0 -0
  78. aethergraph/services/planning/routers/simple_router.py +26 -0
  79. aethergraph/services/rag/facade.py +0 -3
  80. aethergraph/services/scope/scope.py +30 -30
  81. aethergraph/services/scope/scope_factory.py +15 -7
  82. aethergraph/services/skills/__init__.py +0 -0
  83. aethergraph/services/skills/skill_registry.py +465 -0
  84. aethergraph/services/skills/skills.py +220 -0
  85. aethergraph/services/skills/utils.py +194 -0
  86. aethergraph/storage/artifacts/artifact_index_jsonl.py +16 -10
  87. aethergraph/storage/artifacts/artifact_index_sqlite.py +12 -2
  88. aethergraph/storage/docstore/sqlite_doc_sync.py +1 -1
  89. aethergraph/storage/memory/event_persist.py +42 -2
  90. aethergraph/storage/memory/fs_persist.py +32 -2
  91. aethergraph/storage/search_backend/__init__.py +0 -0
  92. aethergraph/storage/search_backend/generic_vector_backend.py +230 -0
  93. aethergraph/storage/search_backend/null_backend.py +34 -0
  94. aethergraph/storage/search_backend/sqlite_lexical_backend.py +387 -0
  95. aethergraph/storage/search_backend/utils.py +31 -0
  96. aethergraph/storage/search_factory.py +75 -0
  97. aethergraph/storage/vector_index/faiss_index.py +72 -4
  98. aethergraph/storage/vector_index/sqlite_index.py +521 -52
  99. aethergraph/storage/vector_index/sqlite_index_vanila.py +311 -0
  100. aethergraph/storage/vector_index/utils.py +22 -0
  101. {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/METADATA +1 -1
  102. {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/RECORD +107 -63
  103. {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/WHEEL +1 -1
  104. aethergraph/plugins/agents/default_chat_agent copy.py +0 -90
  105. aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +0 -1
  106. aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +0 -400
  107. aethergraph/services/eventhub/event_hub.py +0 -76
  108. aethergraph/services/llm/generic_client copy.py +0 -691
  109. aethergraph/services/prompts/file_store.py +0 -41
  110. {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/entry_points.txt +0 -0
  111. {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/LICENSE +0 -0
  112. {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/NOTICE +0 -0
  113. {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,465 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from aethergraph.services.skills.utils import parse_skill_markdown
7
+
8
+ from .skills import Skill
9
+
10
+
11
+ class SkillRegistry:
12
+ """
13
+ Registry for reusable prompt skills.
14
+
15
+ Supports:
16
+ - Registering inline Skill objects.
17
+ - Loading skills from one or more skill directories (markdown files).
18
+ - Retrieving entire skills or specific sections via dot-path keys.
19
+ - Simple filtering by tags/domain/modes.
20
+ """
21
+
22
+ def __init__(self) -> None:
23
+ self._skills: dict[str, Skill] = {}
24
+
25
+ def register(self, skill: Skill, *, overwrite: bool = False) -> None:
26
+ """
27
+ Register a Skill object.
28
+
29
+ This method allows you to add a `Skill` object to the registry. If a
30
+ skill with the same ID already exists and `overwrite` is set to `False`,
31
+ a `ValueError` will be raised.
32
+
33
+ Examples:
34
+ Registering a skill object:
35
+ ```python
36
+ skill = Skill(
37
+ id="example.skill",
38
+ title="Example Skill",
39
+ description="An example skill for demonstration purposes.",
40
+ tags=["example", "demo"],
41
+ domain="general",
42
+ modes=["chat"],
43
+ )
44
+ registry.register(skill)
45
+ ```
46
+
47
+ Overwriting an existing skill:
48
+ ```python
49
+ registry.register(skill, overwrite=True)
50
+ ```
51
+
52
+ Args:
53
+ skill: The `Skill` object to register. (Required)
54
+ overwrite: Whether to overwrite an existing skill with the same ID. (Optional)
55
+ Defaults to `False`.
56
+ """
57
+ if not overwrite and skill.id in self._skills:
58
+ raise ValueError(f"Skill with id={skill.id!r} already registered")
59
+ self._skills[skill.id] = skill
60
+
61
+ def register_inline(
62
+ self,
63
+ *,
64
+ id: str,
65
+ title: str,
66
+ description: str = "",
67
+ tags: list[str] | None = None,
68
+ domain: str | None = None,
69
+ modes: list[str] | None = None,
70
+ version: str | None = None,
71
+ config: dict[str, Any] | None = None,
72
+ sections: dict[str, str] | None = None,
73
+ overwrite: bool = False,
74
+ ) -> Skill:
75
+ """
76
+ Convenience for defining a Skill entirely in Python.
77
+
78
+ This method allows you to define and register a Skill inline, without
79
+ needing to create a separate markdown file. It is useful for quick
80
+ prototyping or defining simple skills directly in code.
81
+
82
+ Examples:
83
+ Registering a basic coding helper skill:
84
+ ```python
85
+ registry.register_inline(
86
+ id="coding.generic",
87
+ title="Generic coding helper",
88
+ description="Helps with Python code generation and review.",
89
+ tags=["coding"],
90
+ modes=["chat", "coding"],
91
+ sections={
92
+ "chat.system": "You are a helpful coding assistant...",
93
+ "coding.system": "You write code as JSON ...",
94
+ },
95
+ )
96
+ ```
97
+
98
+ Args:
99
+ id: The unique identifier for the skill. (Required)
100
+ title: A short, descriptive title for the skill. (Required)
101
+ description: A longer description of the skill's purpose. (Optional)
102
+ tags: A list of tags for categorization. (Optional)
103
+ domain: The domain or category the skill belongs to. (Optional)
104
+ modes: A list of modes the skill supports (e.g., "chat", "coding"). (Optional)
105
+ version: The version of the skill. (Optional)
106
+ config: A dictionary of additional configuration options. (Optional)
107
+ sections: A dictionary mapping section keys to their content. (Optional)
108
+ overwrite: Whether to overwrite an existing skill with the same ID. (Optional)
109
+
110
+ Returns:
111
+ Skill: The registered `Skill` object.
112
+ """
113
+ skill = Skill(
114
+ id=id,
115
+ title=title,
116
+ description=description,
117
+ tags=list(tags or []),
118
+ domain=domain,
119
+ modes=list(modes or []),
120
+ version=version,
121
+ config=dict(config or {}),
122
+ sections=dict(sections or {}),
123
+ raw_markdown=None,
124
+ path=None,
125
+ )
126
+ self.register(skill, overwrite=overwrite)
127
+ return skill
128
+
129
+ def load_file(self, path: str | Path, *, overwrite: bool = False) -> Skill:
130
+ """
131
+ Load a single .md skill file and register it.
132
+
133
+ This method reads the content of a markdown file, parses it into a
134
+ `Skill` object, and registers it in the skill registry.
135
+
136
+ Examples:
137
+ Loading a skill from a file:
138
+ ```python
139
+ skill = registry.load_file("path/to/skill.md")
140
+ ```
141
+
142
+ Args:
143
+ path: The file path to the markdown skill file. (Required)
144
+ overwrite: Whether to overwrite an existing skill with the same ID. (Optional)
145
+
146
+ Returns:
147
+ Skill: The registered `Skill` object.
148
+ """
149
+ p = Path(path)
150
+ text = p.read_text(encoding="utf-8")
151
+ skill = parse_skill_markdown(text, path=p)
152
+ self.register(skill, overwrite=overwrite)
153
+ return skill
154
+
155
+ def load_path(
156
+ self,
157
+ root: str | Path,
158
+ *,
159
+ pattern: str = "*.md",
160
+ recursive: bool = True,
161
+ overwrite: bool = False,
162
+ ) -> list[Skill]:
163
+ """
164
+ Load all skill markdown files under a directory.
165
+
166
+ This method scans the specified directory for markdown files matching
167
+ the given pattern, parses them into `Skill` objects, and registers them
168
+ in the skill registry.
169
+
170
+ Examples:
171
+ Loading all skills from a directory recursively:
172
+ ```python
173
+ skills = registry.load_path("path/to/skills", pattern="*.md", recursive=True)
174
+ ```
175
+
176
+ Loading skills from a directory without recursion:
177
+ ```python
178
+ skills = registry.load_path("path/to/skills", recursive=False)
179
+ ```
180
+
181
+ Args:
182
+ root: The root directory to scan for markdown files. (Required)
183
+ pattern: The glob pattern to match files (e.g., `"*.md"`). (Optional)
184
+ Defaults to `"*.md"`.
185
+ recursive: Whether to scan directories recursively. (Optional)
186
+ Defaults to `True`.
187
+ overwrite: Whether to overwrite existing skills with the same ID. (Optional)
188
+ Defaults to `False`.
189
+
190
+ Returns:
191
+ list[Skill]: A list of all successfully loaded and registered `Skill` objects.
192
+ """
193
+ root_path = Path(root)
194
+ if recursive: # noqa: SIM108
195
+ files = list(root_path.rglob(pattern))
196
+ else:
197
+ files = list(root_path.glob(pattern))
198
+
199
+ loaded: list[Skill] = []
200
+ for f in sorted(files):
201
+ loaded.append(self.load_file(f, overwrite=overwrite))
202
+ return loaded
203
+
204
+ def get(self, skill_id: str) -> Skill | None:
205
+ """
206
+ Get a registered Skill by id.
207
+
208
+ This method retrieves a `Skill` object from the registry using its unique
209
+ identifier. If the skill is not found, it returns `None`.
210
+
211
+ Examples:
212
+ Retrieving a skill by its ID:
213
+ ```python
214
+ skill = registry.get("coding.generic")
215
+ if skill:
216
+ print(skill.title)
217
+ ```
218
+
219
+ Args:
220
+ skill_id: The unique identifier of the skill to retrieve. (Required)
221
+
222
+ Returns:
223
+ Skill | None: The `Skill` object if found, otherwise `None`.
224
+ """
225
+ return self._skills.get(skill_id)
226
+
227
+ def require(self, skill_id: str) -> Skill:
228
+ """
229
+ Retrieve a registered Skill by its unique identifier.
230
+
231
+ This method ensures that the requested Skill exists in the registry.
232
+ If the Skill is not found, it raises a KeyError.
233
+
234
+ Examples:
235
+ Retrieving a skill by its ID:
236
+ ```python
237
+ skill = registry.require("coding.generic")
238
+ print(skill.title)
239
+ ```
240
+
241
+ Args:
242
+ skill_id: The unique identifier of the skill to retrieve. (Required)
243
+
244
+ Returns:
245
+ Skill: The `Skill` object corresponding to the given ID.
246
+
247
+ Raises:
248
+ KeyError: If the skill with the specified ID is not found.
249
+ """
250
+ skill = self.get(skill_id)
251
+ if skill is None:
252
+ raise KeyError(f"Skill with id={skill_id!r} not found")
253
+ return skill
254
+
255
+ def all(self) -> list[Skill]:
256
+ """
257
+ Return all registered Skills.
258
+
259
+ This method retrieves all `Skill` objects currently registered in the
260
+ skill registry and returns them as a list.
261
+
262
+ Examples:
263
+ Retrieving all registered skills:
264
+ ```python
265
+ skills = registry.all()
266
+ for skill in skills:
267
+ print(skill.id, skill.title)
268
+ ```
269
+
270
+ Returns:
271
+ list[Skill]: A list of all registered `Skill` objects.
272
+ """
273
+ return list(self._skills.values())
274
+
275
+ def ids(self) -> list[str]:
276
+ """
277
+ Return all registered Skill ids.
278
+
279
+ This method retrieves the unique identifiers of all `Skill` objects
280
+ currently registered in the skill registry and returns them as a sorted list.
281
+
282
+ Examples:
283
+ Retrieving all skill IDs:
284
+ ```python
285
+ skill_ids = registry.ids()
286
+ print(skill_ids)
287
+ ```
288
+
289
+ Returns:
290
+ list[str]: A sorted list of all registered skill IDs.
291
+ """
292
+ return sorted(self._skills.keys())
293
+
294
+ # -------------- section helpers ----------------
295
+ def section(self, skill_id: str, section_key: str, default: str = "") -> str:
296
+ """
297
+ Return a section for a given skill, or default.
298
+
299
+ This method retrieves the content of a specific section within a skill
300
+ by its unique identifier and section key. If the skill or section is not
301
+ found, it returns the provided default value.
302
+
303
+ Examples:
304
+ Retrieving a section from a skill:
305
+ ```python
306
+ section_content = registry.section("coding.generic", "chat.system")
307
+ print(section_content)
308
+ ```
309
+
310
+ Providing a default value if the section is missing:
311
+ ```python
312
+ section_content = registry.section("nonexistent.skill", "missing.section", default="Default content")
313
+ ```
314
+
315
+ Args:
316
+ skill_id: The unique identifier of the skill. (Required)
317
+ section_key: The key of the section to retrieve. (Required)
318
+ default: The value to return if the skill or section is not found. (Optional)
319
+
320
+ Returns:
321
+ str: The content of the section if found, otherwise the default value.
322
+ """
323
+ skill = self.get(skill_id)
324
+ if not skill:
325
+ return default
326
+ return skill.section(section_key, default=default)
327
+
328
+ def compile_prompt(
329
+ self,
330
+ skill_id: str,
331
+ *section_keys: str,
332
+ separator: str = "\n\n",
333
+ fallback_keys: list[str] | None = None,
334
+ ):
335
+ """
336
+ Shortcut for Skill.compile_prompt(...) by id.
337
+
338
+ This method compiles a prompt by combining multiple sections of a skill
339
+ identified by its unique ID. It allows you to specify the sections to
340
+ include, the separator to use between sections, and fallback keys for
341
+ missing sections.
342
+
343
+ Examples:
344
+ Compiling a prompt with specific sections:
345
+ ```python
346
+ prompt = registry.compile_prompt(
347
+ "coding.generic",
348
+ "chat.system",
349
+ "chat.user",
350
+ )
351
+ ```
352
+
353
+ Using fallback keys for missing sections:
354
+ ```python
355
+ prompt = registry.compile_prompt(
356
+ "coding.generic",
357
+ "chat.system",
358
+ "chat.user",
359
+ fallback_keys=["default.system", "default.user"]
360
+ )
361
+ ```
362
+
363
+ Args:
364
+ skill_id: The unique identifier of the skill. (Required)
365
+ *section_keys: The keys of the sections to include in the prompt. (Required)
366
+ separator: The string to use as a separator between sections. (Optional)
367
+ Defaults to `double newline`.
368
+ fallback_keys: A list of fallback section keys to use if a section
369
+ is missing. (Optional)
370
+
371
+ Returns:
372
+ str: The compiled prompt as a single string.
373
+ """
374
+ skill = self.require(skill_id)
375
+ return skill.compile_prompt(
376
+ *section_keys,
377
+ separator=separator,
378
+ fallback_keys=fallback_keys,
379
+ )
380
+
381
+ def find(
382
+ self,
383
+ *,
384
+ tag: str | None = None,
385
+ domain: str | None = None,
386
+ mode: str | None = None,
387
+ predicate: callable | None = None,
388
+ ) -> list[Skill]:
389
+ """
390
+ Filter skills by tag, domain, mode, and/or a custom predicate.
391
+
392
+ This method allows you to filter registered skills based on specific
393
+ criteria such as tags, domain, mode, or a custom predicate function.
394
+
395
+ Examples:
396
+ Finding skills with a specific tag and mode:
397
+ ```python
398
+ skills = registry.find(tag="surrogate", mode="planning")
399
+ ```
400
+
401
+ Using a custom predicate to filter skills:
402
+ ```python
403
+ skills = registry.find(predicate=lambda s: "example" in s.title)
404
+ ```
405
+
406
+ Args:
407
+ tag: A string representing the tag to filter by. (Optional)
408
+ domain: The domain or category to filter by. (Optional)
409
+ mode: The mode (e.g., "chat", "coding") to filter by. (Optional)
410
+ predicate: A callable that takes a `Skill` object and returns a
411
+ boolean indicating whether the skill matches the criteria. (Optional)
412
+
413
+ Returns:
414
+ list[Skill]: A list of `Skill` objects that match the specified criteria.
415
+
416
+ """
417
+ out: list[Skill] = []
418
+ for s in self._skills.values():
419
+ if tag and tag not in s.tags:
420
+ continue
421
+ if domain and s.domain != domain:
422
+ continue
423
+ if mode and mode not in s.modes:
424
+ continue
425
+ if predicate and not predicate(s):
426
+ continue
427
+ out.append(s)
428
+ return out
429
+
430
+ def describe(self) -> list[dict[str, Any]]:
431
+ """
432
+ Return a compact description of all registered skills.
433
+
434
+ This method provides a summary of all skills currently registered in
435
+ the registry, including their metadata such as ID, title, description,
436
+ tags, domain, modes, version, and sections.
437
+
438
+ Examples:
439
+ Retrieving skill descriptions for debugging or UI purposes:
440
+ ```python
441
+ descriptions = registry.describe()
442
+ for skill in descriptions:
443
+ print(skill["id"], skill["title"])
444
+ ```
445
+
446
+ Returns:
447
+ list[dict[str, Any]]: A list of dictionaries, each containing the
448
+ metadata of a registered skill.
449
+ """
450
+ info: list[dict[str, Any]] = []
451
+ for s in self._skills.values():
452
+ info.append(
453
+ {
454
+ "id": s.id,
455
+ "title": s.title,
456
+ "description": s.description,
457
+ "tags": s.tags,
458
+ "domain": s.domain,
459
+ "modes": s.modes,
460
+ "version": s.version,
461
+ "path": str(s.path) if s.path else None,
462
+ "sections": sorted(s.sections.keys()),
463
+ }
464
+ )
465
+ return info
@@ -0,0 +1,220 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Mapping
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class Skill:
11
+ """
12
+ Skill represents a reusable prompt "skill" that can be loaded from markdown or defined inline.
13
+ It includes metadata, configuration, and sections of content that can be used to generate prompts.
14
+
15
+ Attributes:
16
+ id (str): Unique identifier for the skill.
17
+ title (str): Title of the skill.
18
+ description (str): Description of the skill. Defaults to an empty string.
19
+ tags (list[str]): Tags associated with the skill. Defaults to an empty list.
20
+ domain (str | None): Domain or category of the skill. Defaults to None.
21
+ modes (list[str]): Modes in which the skill can be used (e.g., 'chat', 'planning', 'coding'). Defaults to an empty list.
22
+ version (str | None): Version of the skill. Defaults to None.
23
+ config (dict[str, Any]): Additional configuration for the skill. Defaults to an empty dictionary.
24
+ sections (dict[str, str]): Parsed sections of the skill, keyed by dot-paths.
25
+ raw_markdown (str | None): Raw markdown content of the skill. Defaults to None.
26
+ path (Path | None): File path of the skill, if loaded from a file. Defaults to None.
27
+
28
+ Methods:
29
+ section(key: str, default: str = "") -> str:
30
+ Retrieve a specific section by its dot-path key. Returns the default value if the section is missing.
31
+ has_section(key: str) -> bool:
32
+ Check if a specific section exists in the skill.
33
+ compile_prompt(*section_keys: str, separator: str, fallback_keys: Iterable[str] | None = None) -> str:
34
+ Compile a prompt by concatenating specified sections. If no sections are specified, compiles the entire skill.
35
+ from_dict(meta: Mapping[str, Any], sections: Mapping[str, str], *, raw_markdown: str | None = None, path: Path | None = None) -> Skill:
36
+ Class method to create a Skill instance from metadata and sections. Useful for programmatically defining skills.
37
+ """
38
+
39
+ id: str
40
+ title: str
41
+ description: str = ""
42
+ tags: list[str] = field(default_factory=list)
43
+ domain: str | None = None
44
+ modes: list[str] = field(default_factory=list) # e.g. ['chat', 'planning', 'coding']
45
+ version: str | None = None
46
+ config: dict[str, Any] = field(default_factory=dict)
47
+
48
+ # parsed contents
49
+ sections: dict[str, str] = field(default_factory=dict)
50
+ raw_markdown: str | None = None
51
+ path: Path | None = None
52
+
53
+ # helpers
54
+ def section(self, key: str, default: str = "") -> str:
55
+ """
56
+ Retrieve a specific section value by its dot-path key, or return a default value if the key is missing.
57
+ This method allows accessing nested sections of a configuration or data structure
58
+ using a dot-separated key path. If the specified key is not found, the provided
59
+ default value is returned.
60
+
61
+ Examples:
62
+ Accessing a specific section:
63
+ ```python
64
+ value = obj.section("chat.system")
65
+ ```
66
+
67
+ Providing a default value if the key is missing:
68
+ ```python
69
+ value = obj.section("nonexistent.key", default="Default Value")
70
+ ```
71
+
72
+ Args:
73
+ key: A dot-separated string representing the path to the desired section.
74
+ default: The value to return if the key is not found (default: an empty string).
75
+
76
+ Returns:
77
+ The value of the specified section if found, otherwise the default value.
78
+
79
+ Notes:
80
+ This method assumes that the `sections` attribute is a dictionary-like object
81
+ that supports the `get` method for key-value retrieval.
82
+ """
83
+ return self.sections.get(key, default)
84
+
85
+ def has_section(self, key: str) -> bool:
86
+ """
87
+ Check if a specific section exists in the skill.
88
+
89
+ This method determines whether a given dot-path key corresponds to an
90
+ existing section in the `sections` attribute.
91
+
92
+ Examples:
93
+ Checking for the existence of a section:
94
+ ```python
95
+ exists = skill.has_section("chat.system")
96
+ ```
97
+
98
+ Using the method to conditionally access a section:
99
+ ```python
100
+ if skill.has_section("chat.example"):
101
+ example = skill.section("chat.example")
102
+ ```
103
+
104
+ Args:
105
+ key: A dot-separated string representing the path to the desired section.
106
+
107
+ Returns:
108
+ bool: True if the section exists, False otherwise.
109
+ """
110
+ return key in self.sections
111
+
112
+ def compile_prompt(
113
+ self,
114
+ *section_keys: str,
115
+ separator: str = "\n\n",
116
+ fallback_keys: Iterable[str] | None = None,
117
+ ) -> str:
118
+ """
119
+ Compile a prompt by concatenating specified sections.
120
+
121
+ Examples
122
+ --------
123
+ 1) Only specific sections:
124
+
125
+ prompt = skill.compile_prompt(
126
+ "chat.system",
127
+ "chat.example",
128
+ )
129
+
130
+ 2) Full skill (lazy mode):
131
+
132
+ # No section keys -> entire skill:
133
+ prompt = skill.compile_prompt()
134
+
135
+ Behavior
136
+ --------
137
+ - If `section_keys` are provided:
138
+ - Concatenate those sections in order, skipping any that are missing.
139
+ - If `section_keys` is empty and `fallback_keys` is provided:
140
+ - Use `fallback_keys` as the section list.
141
+ - If both `section_keys` and `fallback_keys` are empty / None:
142
+ - Return the *entire* skill by concatenating:
143
+ 1) The optional "body" preface (if present), then
144
+ 2) All other sections in lexicographic order of their keys.
145
+
146
+ Returns
147
+ -------
148
+ str
149
+ The compiled prompt string (may be empty if no sections are found).
150
+ """
151
+ keys: list[str] = list(section_keys)
152
+
153
+ if not keys:
154
+ if fallback_keys:
155
+ # Use caller-provided fallback ordering.
156
+ keys = list(fallback_keys)
157
+ else:
158
+ # "Full skill" mode: include everything.
159
+ all_keys = list(self.sections.keys())
160
+ ordered: list[str] = []
161
+
162
+ # Put "body" first if present.
163
+ if "body" in self.sections:
164
+ ordered.append("body")
165
+ all_keys.remove("body")
166
+
167
+ # Then all other sections in a stable order.
168
+ ordered.extend(sorted(all_keys))
169
+ keys = ordered
170
+
171
+ chunks: list[str] = []
172
+ for key in keys:
173
+ text = self.sections.get(key)
174
+ if text:
175
+ chunks.append(text)
176
+
177
+ return separator.join(chunks).strip()
178
+
179
+ @classmethod
180
+ def from_dict(
181
+ cls,
182
+ meta: Mapping[str, Any],
183
+ sections: Mapping[str, str],
184
+ *,
185
+ raw_markdown: str | None = None,
186
+ path: Path | None = None,
187
+ ) -> Skill:
188
+ """
189
+ Create a Skill from Python metadata + sections.
190
+ Useful for inline / programmatic skills.
191
+ """
192
+ skill_id = str(meta.get("id") or meta.get("name") or "").strip()
193
+ if not skill_id:
194
+ raise ValueError("Skill metadata must include a non-empty 'id' field.")
195
+
196
+ title = str(meta.get("title") or skill_id).strip()
197
+ description = str(meta.get("description") or "").strip()
198
+
199
+ tags = list(meta.get("tags") or [])
200
+ domain = meta.get("domain")
201
+ modes = list(meta.get("modes") or [])
202
+ version = meta.get("version")
203
+
204
+ # Any extra fields in meta go into config
205
+ know_keys = {"id", "name", "title", "description", "tags", "domain", "modes", "version"}
206
+ config = {k: v for k, v in meta.items() if k not in know_keys}
207
+
208
+ return cls(
209
+ id=skill_id,
210
+ title=title,
211
+ description=description,
212
+ tags=tags,
213
+ domain=domain,
214
+ modes=modes,
215
+ version=version,
216
+ config=config,
217
+ sections=dict(sections),
218
+ raw_markdown=raw_markdown,
219
+ path=path,
220
+ )