aethr 0.1.0__tar.gz
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.
- aethr-0.1.0/.gitignore +7 -0
- aethr-0.1.0/LICENSE +21 -0
- aethr-0.1.0/PKG-INFO +218 -0
- aethr-0.1.0/README.md +188 -0
- aethr-0.1.0/aethr/__init__.py +3 -0
- aethr-0.1.0/aethr/cli.py +132 -0
- aethr-0.1.0/aethr/config.py +81 -0
- aethr-0.1.0/aethr/context.py +131 -0
- aethr-0.1.0/aethr/executor.py +111 -0
- aethr-0.1.0/aethr/llm.py +52 -0
- aethr-0.1.0/aethr/prompts.py +65 -0
- aethr-0.1.0/aethr/workflow.py +49 -0
- aethr-0.1.0/aethr/workflows/__init__.py +1 -0
- aethr-0.1.0/aethr/workflows/add_tests.yaml +30 -0
- aethr-0.1.0/aethr/workflows/custom.yaml +11 -0
- aethr-0.1.0/aethr/workflows/debug_failing_test.yaml +31 -0
- aethr-0.1.0/aethr/workflows/docs_sync.yaml +24 -0
- aethr-0.1.0/aethr/workflows/plan_implement_review.yaml +21 -0
- aethr-0.1.0/aethr/workflows/review_existing_diff.yaml +13 -0
- aethr-0.1.0/examples/add-tests.yaml +30 -0
- aethr-0.1.0/examples/docs-sync.yaml +24 -0
- aethr-0.1.0/examples/review-existing-diff.yaml +13 -0
- aethr-0.1.0/pyproject.toml +59 -0
- aethr-0.1.0/tests/test_cli.py +12 -0
- aethr-0.1.0/tests/test_config.py +93 -0
- aethr-0.1.0/tests/test_context.py +52 -0
- aethr-0.1.0/tests/test_executor.py +56 -0
- aethr-0.1.0/tests/test_llm.py +10 -0
- aethr-0.1.0/tests/test_prompts.py +19 -0
- aethr-0.1.0/tests/test_workflow.py +60 -0
aethr-0.1.0/.gitignore
ADDED
aethr-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Archana Pradeep
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
aethr-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aethr
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight CLI for explicit, reproducible AI coding workflows.
|
|
5
|
+
Project-URL: Homepage, https://github.com/archthegit/Relay
|
|
6
|
+
Project-URL: Repository, https://github.com/archthegit/Relay
|
|
7
|
+
Project-URL: Issues, https://github.com/archthegit/Relay/issues
|
|
8
|
+
Author: Archana Pradeep
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,cli,coding,llm,workflow
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: litellm>=1.60.0
|
|
23
|
+
Requires-Dist: pydantic>=2.10.0
|
|
24
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
25
|
+
Requires-Dist: rich>=13.9.0
|
|
26
|
+
Requires-Dist: typer>=0.15.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Aethr
|
|
32
|
+
|
|
33
|
+
A tiny CLI for running explicit AI coding workflows from YAML.
|
|
34
|
+
|
|
35
|
+
## Core Idea
|
|
36
|
+
|
|
37
|
+
Coding with LLMs is not one-shot generation.
|
|
38
|
+
|
|
39
|
+
Real development is:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
plan -> implement -> review -> iterate
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Aethr makes those workflows programmable. A run is just:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
task + workflow + explicit context + model routing
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Aethr is stateless. The only project file it creates is `.aethr.yaml`.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install aethr
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For local development:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install -e ".[dev]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Quickstart
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
aethr init review-existing-diff
|
|
69
|
+
aethr run "review my current changes before I commit"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Aethr copies a YAML preset into `.aethr.yaml`. Edit it like any other project
|
|
73
|
+
file.
|
|
74
|
+
|
|
75
|
+
## How Aethr Works
|
|
76
|
+
|
|
77
|
+
- **Task**: the instruction passed on the command line.
|
|
78
|
+
- **Workflow**: the YAML file that defines ordered steps.
|
|
79
|
+
- **Steps**: sequential units of work. Aethr runs them in order.
|
|
80
|
+
- **Roles**: named responsibilities such as `planner`, `reviewer`, or `writer`.
|
|
81
|
+
- **Context**: explicit repo input declared per step.
|
|
82
|
+
- **Model routing**: each role can point at a different LiteLLM model.
|
|
83
|
+
|
|
84
|
+
Each step receives the task, prior step outputs, and its declared context. The
|
|
85
|
+
step result stays in memory and is printed to the terminal.
|
|
86
|
+
|
|
87
|
+
## Example Workflow Config
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
workflow: review-existing-diff
|
|
91
|
+
|
|
92
|
+
roles:
|
|
93
|
+
reviewer: Review the provided task context as if it were an existing diff.
|
|
94
|
+
|
|
95
|
+
models:
|
|
96
|
+
reviewer: openai:gpt-5.5
|
|
97
|
+
|
|
98
|
+
steps:
|
|
99
|
+
- id: review
|
|
100
|
+
role: reviewer
|
|
101
|
+
context:
|
|
102
|
+
- git_diff
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Built-In Workflows
|
|
106
|
+
|
|
107
|
+
- `plan-implement-review`: plan a task, propose an implementation, review it.
|
|
108
|
+
- `review-existing-diff`: review the current working tree diff.
|
|
109
|
+
- `debug-failing-test`: diagnose a failing test, propose a fix, review it.
|
|
110
|
+
- `add-tests`: plan, draft, and review focused test coverage.
|
|
111
|
+
- `docs-sync`: update docs from the current diff and README context.
|
|
112
|
+
- `custom`: a minimal one-step workflow to edit freely.
|
|
113
|
+
|
|
114
|
+
List presets:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
aethr init --list
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Initialize another preset:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
aethr init docs-sync --force
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Examples
|
|
127
|
+
|
|
128
|
+
The `examples/` directory contains small workflow files you can copy from:
|
|
129
|
+
|
|
130
|
+
- `examples/review-existing-diff.yaml`
|
|
131
|
+
- `examples/add-tests.yaml`
|
|
132
|
+
- `examples/docs-sync.yaml`
|
|
133
|
+
|
|
134
|
+
## Explicit Context
|
|
135
|
+
|
|
136
|
+
Aethr uses explicit context instead of automatic retrieval. That keeps runs easy
|
|
137
|
+
to understand: the YAML shows exactly what each step can see.
|
|
138
|
+
|
|
139
|
+
Supported context sources:
|
|
140
|
+
|
|
141
|
+
- `git_diff`: runs `git diff --no-ext-diff`.
|
|
142
|
+
- `file:<path>`: reads one UTF-8 file relative to the project root.
|
|
143
|
+
- `glob:<pattern>`: reads matching UTF-8 files relative to the project root,
|
|
144
|
+
with a small content cap.
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
steps:
|
|
150
|
+
- id: review-docs
|
|
151
|
+
role: reviewer
|
|
152
|
+
context:
|
|
153
|
+
- git_diff
|
|
154
|
+
- file:README.md
|
|
155
|
+
- glob:docs/**/*.md
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Missing files, empty diffs, non-git directories, and unreadable files appear as
|
|
159
|
+
clear placeholder notes in the prompt.
|
|
160
|
+
|
|
161
|
+
## Prompt Previewing
|
|
162
|
+
|
|
163
|
+
Use `--show-prompt` to see exactly what Aethr would send to each model:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
aethr run "review my current changes before I commit" --show-prompt
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Aethr does not call models in prompt preview mode. For later steps, it uses a
|
|
170
|
+
clear placeholder where real previous step output would appear.
|
|
171
|
+
|
|
172
|
+
## Mock Mode
|
|
173
|
+
|
|
174
|
+
Aethr works without API keys by returning deterministic mock responses.
|
|
175
|
+
|
|
176
|
+
Use the models configured in `.aethr.yaml`:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
AETHR_LIVE=1 aethr run "review my current changes"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Override every configured model with one LiteLLM model:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
AETHR_MODEL=openai:gpt-5.5 aethr run "review my current changes"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Philosophy
|
|
189
|
+
|
|
190
|
+
Aethr should feel like:
|
|
191
|
+
|
|
192
|
+
- `git`
|
|
193
|
+
- `pytest`
|
|
194
|
+
- `rg`
|
|
195
|
+
- `cargo`
|
|
196
|
+
|
|
197
|
+
It should not feel like:
|
|
198
|
+
|
|
199
|
+
- an agent framework
|
|
200
|
+
- an autonomous coding platform
|
|
201
|
+
- an AI operating system
|
|
202
|
+
|
|
203
|
+
Aethr intentionally avoids persistence, replay systems, caches, plugins, DAGs,
|
|
204
|
+
async runtimes, vector search, automatic retrieval, memory systems, and agent
|
|
205
|
+
abstractions.
|
|
206
|
+
|
|
207
|
+
## Architecture
|
|
208
|
+
|
|
209
|
+
```text
|
|
210
|
+
aethr/
|
|
211
|
+
cli.py
|
|
212
|
+
config.py
|
|
213
|
+
context.py
|
|
214
|
+
executor.py
|
|
215
|
+
llm.py
|
|
216
|
+
prompts.py
|
|
217
|
+
workflow.py
|
|
218
|
+
```
|
aethr-0.1.0/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Aethr
|
|
2
|
+
|
|
3
|
+
A tiny CLI for running explicit AI coding workflows from YAML.
|
|
4
|
+
|
|
5
|
+
## Core Idea
|
|
6
|
+
|
|
7
|
+
Coding with LLMs is not one-shot generation.
|
|
8
|
+
|
|
9
|
+
Real development is:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
plan -> implement -> review -> iterate
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Aethr makes those workflows programmable. A run is just:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
task + workflow + explicit context + model routing
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Aethr is stateless. The only project file it creates is `.aethr.yaml`.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install aethr
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For local development:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install -e ".[dev]"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
aethr init review-existing-diff
|
|
39
|
+
aethr run "review my current changes before I commit"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Aethr copies a YAML preset into `.aethr.yaml`. Edit it like any other project
|
|
43
|
+
file.
|
|
44
|
+
|
|
45
|
+
## How Aethr Works
|
|
46
|
+
|
|
47
|
+
- **Task**: the instruction passed on the command line.
|
|
48
|
+
- **Workflow**: the YAML file that defines ordered steps.
|
|
49
|
+
- **Steps**: sequential units of work. Aethr runs them in order.
|
|
50
|
+
- **Roles**: named responsibilities such as `planner`, `reviewer`, or `writer`.
|
|
51
|
+
- **Context**: explicit repo input declared per step.
|
|
52
|
+
- **Model routing**: each role can point at a different LiteLLM model.
|
|
53
|
+
|
|
54
|
+
Each step receives the task, prior step outputs, and its declared context. The
|
|
55
|
+
step result stays in memory and is printed to the terminal.
|
|
56
|
+
|
|
57
|
+
## Example Workflow Config
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
workflow: review-existing-diff
|
|
61
|
+
|
|
62
|
+
roles:
|
|
63
|
+
reviewer: Review the provided task context as if it were an existing diff.
|
|
64
|
+
|
|
65
|
+
models:
|
|
66
|
+
reviewer: openai:gpt-5.5
|
|
67
|
+
|
|
68
|
+
steps:
|
|
69
|
+
- id: review
|
|
70
|
+
role: reviewer
|
|
71
|
+
context:
|
|
72
|
+
- git_diff
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Built-In Workflows
|
|
76
|
+
|
|
77
|
+
- `plan-implement-review`: plan a task, propose an implementation, review it.
|
|
78
|
+
- `review-existing-diff`: review the current working tree diff.
|
|
79
|
+
- `debug-failing-test`: diagnose a failing test, propose a fix, review it.
|
|
80
|
+
- `add-tests`: plan, draft, and review focused test coverage.
|
|
81
|
+
- `docs-sync`: update docs from the current diff and README context.
|
|
82
|
+
- `custom`: a minimal one-step workflow to edit freely.
|
|
83
|
+
|
|
84
|
+
List presets:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
aethr init --list
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Initialize another preset:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
aethr init docs-sync --force
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Examples
|
|
97
|
+
|
|
98
|
+
The `examples/` directory contains small workflow files you can copy from:
|
|
99
|
+
|
|
100
|
+
- `examples/review-existing-diff.yaml`
|
|
101
|
+
- `examples/add-tests.yaml`
|
|
102
|
+
- `examples/docs-sync.yaml`
|
|
103
|
+
|
|
104
|
+
## Explicit Context
|
|
105
|
+
|
|
106
|
+
Aethr uses explicit context instead of automatic retrieval. That keeps runs easy
|
|
107
|
+
to understand: the YAML shows exactly what each step can see.
|
|
108
|
+
|
|
109
|
+
Supported context sources:
|
|
110
|
+
|
|
111
|
+
- `git_diff`: runs `git diff --no-ext-diff`.
|
|
112
|
+
- `file:<path>`: reads one UTF-8 file relative to the project root.
|
|
113
|
+
- `glob:<pattern>`: reads matching UTF-8 files relative to the project root,
|
|
114
|
+
with a small content cap.
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
steps:
|
|
120
|
+
- id: review-docs
|
|
121
|
+
role: reviewer
|
|
122
|
+
context:
|
|
123
|
+
- git_diff
|
|
124
|
+
- file:README.md
|
|
125
|
+
- glob:docs/**/*.md
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Missing files, empty diffs, non-git directories, and unreadable files appear as
|
|
129
|
+
clear placeholder notes in the prompt.
|
|
130
|
+
|
|
131
|
+
## Prompt Previewing
|
|
132
|
+
|
|
133
|
+
Use `--show-prompt` to see exactly what Aethr would send to each model:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
aethr run "review my current changes before I commit" --show-prompt
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Aethr does not call models in prompt preview mode. For later steps, it uses a
|
|
140
|
+
clear placeholder where real previous step output would appear.
|
|
141
|
+
|
|
142
|
+
## Mock Mode
|
|
143
|
+
|
|
144
|
+
Aethr works without API keys by returning deterministic mock responses.
|
|
145
|
+
|
|
146
|
+
Use the models configured in `.aethr.yaml`:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
AETHR_LIVE=1 aethr run "review my current changes"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Override every configured model with one LiteLLM model:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
AETHR_MODEL=openai:gpt-5.5 aethr run "review my current changes"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Philosophy
|
|
159
|
+
|
|
160
|
+
Aethr should feel like:
|
|
161
|
+
|
|
162
|
+
- `git`
|
|
163
|
+
- `pytest`
|
|
164
|
+
- `rg`
|
|
165
|
+
- `cargo`
|
|
166
|
+
|
|
167
|
+
It should not feel like:
|
|
168
|
+
|
|
169
|
+
- an agent framework
|
|
170
|
+
- an autonomous coding platform
|
|
171
|
+
- an AI operating system
|
|
172
|
+
|
|
173
|
+
Aethr intentionally avoids persistence, replay systems, caches, plugins, DAGs,
|
|
174
|
+
async runtimes, vector search, automatic retrieval, memory systems, and agent
|
|
175
|
+
abstractions.
|
|
176
|
+
|
|
177
|
+
## Architecture
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
aethr/
|
|
181
|
+
cli.py
|
|
182
|
+
config.py
|
|
183
|
+
context.py
|
|
184
|
+
executor.py
|
|
185
|
+
llm.py
|
|
186
|
+
prompts.py
|
|
187
|
+
workflow.py
|
|
188
|
+
```
|
aethr-0.1.0/aethr/cli.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Command-line interface for Aethr."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
from aethr import __version__
|
|
10
|
+
from aethr.config import CONFIG_FILE, ConfigError, load_workflow_config
|
|
11
|
+
from aethr.executor import StepPrompt, StepResult, build_workflow_prompts, run_workflow
|
|
12
|
+
from aethr.llm import LLMError
|
|
13
|
+
from aethr.workflow import WorkflowTemplateError, available_workflows, init_workflow
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="Explicit, reproducible AI coding workflows.")
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.callback()
|
|
21
|
+
def main() -> None:
|
|
22
|
+
"""Aethr command group."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command()
|
|
26
|
+
def init(
|
|
27
|
+
preset: Annotated[str | None, typer.Argument(help="Workflow preset to initialize.")] = None,
|
|
28
|
+
force: Annotated[bool, typer.Option("--force", "-f", help="Overwrite an existing .aethr.yaml.")] = False,
|
|
29
|
+
list_only: Annotated[bool, typer.Option("--list", "-l", help="List available workflow presets.")] = False,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Initialize a workflow config from a built-in YAML preset."""
|
|
32
|
+
|
|
33
|
+
workflows = available_workflows()
|
|
34
|
+
if list_only:
|
|
35
|
+
for workflow in workflows:
|
|
36
|
+
console.print(workflow)
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
selected = preset
|
|
40
|
+
if selected is None:
|
|
41
|
+
console.print("Available workflows:")
|
|
42
|
+
for index, workflow in enumerate(workflows, start=1):
|
|
43
|
+
console.print(f" {index}. {workflow}")
|
|
44
|
+
choice = typer.prompt("Select a workflow", default="1")
|
|
45
|
+
selected = _resolve_workflow_choice(choice, workflows)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
path = init_workflow(selected, force=force)
|
|
49
|
+
except WorkflowTemplateError as exc:
|
|
50
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
51
|
+
|
|
52
|
+
console.print(f"Initialized [bold]{path}[/bold] from [cyan]{selected}[/cyan]")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command()
|
|
56
|
+
def run(
|
|
57
|
+
task: Annotated[str, typer.Argument(help="Coding task to run through the pipeline.")],
|
|
58
|
+
show_prompt: Annotated[
|
|
59
|
+
bool,
|
|
60
|
+
typer.Option("--show-prompt", help="Print exact step prompts without calling models."),
|
|
61
|
+
] = False,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Run the configured sequential workflow."""
|
|
64
|
+
|
|
65
|
+
console.print(Panel.fit(task, title="Aethr Task", border_style="cyan"))
|
|
66
|
+
try:
|
|
67
|
+
config = load_workflow_config()
|
|
68
|
+
except ConfigError as exc:
|
|
69
|
+
raise typer.BadParameter(f"{exc}. Run 'aethr init' to create {CONFIG_FILE}.") from exc
|
|
70
|
+
|
|
71
|
+
console.print(f"[bold]Workflow[/bold] {config.workflow}")
|
|
72
|
+
if show_prompt:
|
|
73
|
+
console.print("[bold]Mode[/bold] prompt preview")
|
|
74
|
+
for planned in build_workflow_prompts(task, config):
|
|
75
|
+
_print_step_prompt(planned)
|
|
76
|
+
console.print("[green]Prompt preview complete[/green]")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
results = run_workflow(task, config, on_step_result=_print_step_result)
|
|
81
|
+
except LLMError as exc:
|
|
82
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
83
|
+
console.print(f"[green]Workflow complete[/green] ({len(results)} step results)")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command()
|
|
87
|
+
def version() -> None:
|
|
88
|
+
"""Print the Aethr version."""
|
|
89
|
+
|
|
90
|
+
console.print(f"Aethr {__version__}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _print_step_result(result: StepResult) -> None:
|
|
94
|
+
"""Print one in-memory step result."""
|
|
95
|
+
|
|
96
|
+
title = step_title(result.step_id, result.metadata)
|
|
97
|
+
console.print()
|
|
98
|
+
console.print(Panel(result.content, title=title, border_style="green"))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _print_step_prompt(planned: StepPrompt) -> None:
|
|
102
|
+
"""Print one planned prompt."""
|
|
103
|
+
|
|
104
|
+
title = step_title(planned.step_id, planned.metadata)
|
|
105
|
+
console.print()
|
|
106
|
+
console.print(Panel(planned.prompt, title=title, border_style="yellow"))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def step_title(step_id: str, metadata: dict[str, str]) -> str:
|
|
110
|
+
"""Build a compact terminal title for a step."""
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
f"{step_id} | role={metadata['role']} | "
|
|
114
|
+
f"model={metadata['model']} | context={metadata['context_sources']}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _resolve_workflow_choice(choice: str, workflows: list[str]) -> str:
|
|
119
|
+
"""Resolve an interactive workflow prompt response."""
|
|
120
|
+
|
|
121
|
+
if choice.isdigit():
|
|
122
|
+
index = int(choice)
|
|
123
|
+
if 1 <= index <= len(workflows):
|
|
124
|
+
return workflows[index - 1]
|
|
125
|
+
if choice in workflows:
|
|
126
|
+
return choice
|
|
127
|
+
choices = ", ".join(workflows)
|
|
128
|
+
raise typer.BadParameter(f"Unknown workflow '{choice}'. Available workflows: {choices}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
app()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Workflow configuration schema and loader."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
CONFIG_FILE = ".aethr.yaml"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConfigError(Exception):
|
|
15
|
+
"""Raised when workflow configuration cannot be loaded."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WorkflowStep(BaseModel):
|
|
19
|
+
"""One sequential step in an Aethr workflow."""
|
|
20
|
+
|
|
21
|
+
model_config = ConfigDict(extra="forbid")
|
|
22
|
+
|
|
23
|
+
id: str = Field(min_length=1)
|
|
24
|
+
role: str = Field(min_length=1)
|
|
25
|
+
context: list[str] = Field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WorkflowConfig(BaseModel):
|
|
29
|
+
"""A YAML-defined Aethr workflow."""
|
|
30
|
+
|
|
31
|
+
model_config = ConfigDict(extra="forbid")
|
|
32
|
+
|
|
33
|
+
workflow: str = Field(min_length=1)
|
|
34
|
+
roles: dict[str, str] = Field(default_factory=dict)
|
|
35
|
+
models: dict[str, str] = Field(default_factory=dict)
|
|
36
|
+
steps: list[WorkflowStep] = Field(min_length=1)
|
|
37
|
+
|
|
38
|
+
@model_validator(mode="after")
|
|
39
|
+
def validate_roles(self) -> "WorkflowConfig":
|
|
40
|
+
"""Ensure configured steps and model routes reference known roles."""
|
|
41
|
+
|
|
42
|
+
known_roles = set(self.roles)
|
|
43
|
+
step_roles = {step.role for step in self.steps}
|
|
44
|
+
missing_step_roles = sorted(step_roles - known_roles)
|
|
45
|
+
if missing_step_roles:
|
|
46
|
+
joined = ", ".join(missing_step_roles)
|
|
47
|
+
raise ValueError(f"steps reference undefined roles: {joined}")
|
|
48
|
+
|
|
49
|
+
unknown_model_roles = sorted(set(self.models) - known_roles)
|
|
50
|
+
if unknown_model_roles:
|
|
51
|
+
joined = ", ".join(unknown_model_roles)
|
|
52
|
+
raise ValueError(f"models reference undefined roles: {joined}")
|
|
53
|
+
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_workflow_config(path: Path | str = CONFIG_FILE) -> WorkflowConfig:
|
|
58
|
+
"""Load and validate an Aethr workflow config file."""
|
|
59
|
+
|
|
60
|
+
config_path = Path(path)
|
|
61
|
+
if not config_path.exists():
|
|
62
|
+
raise ConfigError(f"Workflow config not found: {config_path}")
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
|
66
|
+
except yaml.YAMLError as exc:
|
|
67
|
+
raise ConfigError(f"Invalid YAML in {config_path}: {exc}") from exc
|
|
68
|
+
|
|
69
|
+
if raw is None:
|
|
70
|
+
raise ConfigError(f"Workflow config is empty: {config_path}")
|
|
71
|
+
if not isinstance(raw, dict):
|
|
72
|
+
raise ConfigError(f"Workflow config must be a YAML mapping: {config_path}")
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
return WorkflowConfig.model_validate(raw)
|
|
76
|
+
except ValidationError as exc:
|
|
77
|
+
details = "; ".join(
|
|
78
|
+
f"{'.'.join(str(part) for part in error['loc'])}: {error['msg']}"
|
|
79
|
+
for error in exc.errors()
|
|
80
|
+
)
|
|
81
|
+
raise ConfigError(f"Invalid workflow config in {config_path}: {details}") from exc
|