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.
- aethergraph/api/v1/artifacts.py +23 -4
- aethergraph/api/v1/schemas.py +7 -0
- aethergraph/api/v1/session.py +123 -4
- aethergraph/config/config.py +2 -0
- aethergraph/config/search.py +49 -0
- aethergraph/contracts/services/channel.py +18 -1
- aethergraph/contracts/services/execution.py +58 -0
- aethergraph/contracts/services/llm.py +26 -0
- aethergraph/contracts/services/memory.py +10 -4
- aethergraph/contracts/services/planning.py +53 -0
- aethergraph/contracts/storage/event_log.py +8 -0
- aethergraph/contracts/storage/search_backend.py +47 -0
- aethergraph/contracts/storage/vector_index.py +73 -0
- aethergraph/core/graph/action_spec.py +76 -0
- aethergraph/core/graph/graph_fn.py +75 -2
- aethergraph/core/graph/graphify.py +74 -2
- aethergraph/core/runtime/graph_runner.py +2 -1
- aethergraph/core/runtime/node_context.py +66 -3
- aethergraph/core/runtime/node_services.py +8 -0
- aethergraph/core/runtime/run_manager.py +263 -271
- aethergraph/core/runtime/run_types.py +54 -1
- aethergraph/core/runtime/runtime_env.py +35 -14
- aethergraph/core/runtime/runtime_services.py +308 -18
- aethergraph/plugins/agents/default_chat_agent.py +266 -74
- aethergraph/plugins/agents/default_chat_agent_v2.py +487 -0
- aethergraph/plugins/channel/adapters/webui.py +69 -21
- aethergraph/plugins/channel/routes/webui_routes.py +8 -48
- aethergraph/runtime/__init__.py +12 -0
- aethergraph/server/app_factory.py +3 -0
- aethergraph/server/ui_static/assets/index-CFktGdbW.js +4913 -0
- aethergraph/server/ui_static/assets/index-DcfkFlTA.css +1 -0
- aethergraph/server/ui_static/index.html +2 -2
- aethergraph/services/artifacts/facade.py +157 -21
- aethergraph/services/artifacts/types.py +35 -0
- aethergraph/services/artifacts/utils.py +42 -0
- aethergraph/services/channel/channel_bus.py +3 -1
- aethergraph/services/channel/event_hub copy.py +55 -0
- aethergraph/services/channel/event_hub.py +81 -0
- aethergraph/services/channel/factory.py +3 -2
- aethergraph/services/channel/session.py +709 -74
- aethergraph/services/container/default_container.py +69 -7
- aethergraph/services/execution/__init__.py +0 -0
- aethergraph/services/execution/local_python.py +118 -0
- aethergraph/services/indices/__init__.py +0 -0
- aethergraph/services/indices/global_indices.py +21 -0
- aethergraph/services/indices/scoped_indices.py +292 -0
- aethergraph/services/llm/generic_client.py +342 -46
- aethergraph/services/llm/generic_embed_client.py +359 -0
- aethergraph/services/llm/types.py +3 -1
- aethergraph/services/memory/distillers/llm_long_term.py +60 -109
- aethergraph/services/memory/distillers/llm_long_term_v1.py +180 -0
- aethergraph/services/memory/distillers/llm_meta_summary.py +57 -266
- aethergraph/services/memory/distillers/llm_meta_summary_v1.py +342 -0
- aethergraph/services/memory/distillers/long_term.py +48 -131
- aethergraph/services/memory/distillers/long_term_v1.py +170 -0
- aethergraph/services/memory/facade/chat.py +18 -8
- aethergraph/services/memory/facade/core.py +159 -19
- aethergraph/services/memory/facade/distillation.py +86 -31
- aethergraph/services/memory/facade/retrieval.py +100 -1
- aethergraph/services/memory/factory.py +4 -1
- aethergraph/services/planning/__init__.py +0 -0
- aethergraph/services/planning/action_catalog.py +271 -0
- aethergraph/services/planning/bindings.py +56 -0
- aethergraph/services/planning/dependency_index.py +65 -0
- aethergraph/services/planning/flow_validator.py +263 -0
- aethergraph/services/planning/graph_io_adapter.py +150 -0
- aethergraph/services/planning/input_parser.py +312 -0
- aethergraph/services/planning/missing_inputs.py +28 -0
- aethergraph/services/planning/node_planner.py +613 -0
- aethergraph/services/planning/orchestrator.py +112 -0
- aethergraph/services/planning/plan_executor.py +506 -0
- aethergraph/services/planning/plan_types.py +321 -0
- aethergraph/services/planning/planner.py +617 -0
- aethergraph/services/planning/planner_service.py +369 -0
- aethergraph/services/planning/planning_context_builder.py +43 -0
- aethergraph/services/planning/quick_actions.py +29 -0
- aethergraph/services/planning/routers/__init__.py +0 -0
- aethergraph/services/planning/routers/simple_router.py +26 -0
- aethergraph/services/rag/facade.py +0 -3
- aethergraph/services/scope/scope.py +30 -30
- aethergraph/services/scope/scope_factory.py +15 -7
- aethergraph/services/skills/__init__.py +0 -0
- aethergraph/services/skills/skill_registry.py +465 -0
- aethergraph/services/skills/skills.py +220 -0
- aethergraph/services/skills/utils.py +194 -0
- aethergraph/storage/artifacts/artifact_index_jsonl.py +16 -10
- aethergraph/storage/artifacts/artifact_index_sqlite.py +12 -2
- aethergraph/storage/docstore/sqlite_doc_sync.py +1 -1
- aethergraph/storage/memory/event_persist.py +42 -2
- aethergraph/storage/memory/fs_persist.py +32 -2
- aethergraph/storage/search_backend/__init__.py +0 -0
- aethergraph/storage/search_backend/generic_vector_backend.py +230 -0
- aethergraph/storage/search_backend/null_backend.py +34 -0
- aethergraph/storage/search_backend/sqlite_lexical_backend.py +387 -0
- aethergraph/storage/search_backend/utils.py +31 -0
- aethergraph/storage/search_factory.py +75 -0
- aethergraph/storage/vector_index/faiss_index.py +72 -4
- aethergraph/storage/vector_index/sqlite_index.py +521 -52
- aethergraph/storage/vector_index/sqlite_index_vanila.py +311 -0
- aethergraph/storage/vector_index/utils.py +22 -0
- {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/METADATA +1 -1
- {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/RECORD +107 -63
- {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/WHEEL +1 -1
- aethergraph/plugins/agents/default_chat_agent copy.py +0 -90
- aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +0 -1
- aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +0 -400
- aethergraph/services/eventhub/event_hub.py +0 -76
- aethergraph/services/llm/generic_client copy.py +0 -691
- aethergraph/services/prompts/file_store.py +0 -41
- {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/entry_points.txt +0 -0
- {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/LICENSE +0 -0
- {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/NOTICE +0 -0
- {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
|
+
)
|