readyagentsdev 0.8.2__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.
- readyagents/__init__.py +38 -0
- readyagents/__main__.py +4 -0
- readyagents/audit.py +67 -0
- readyagents/cli.py +1050 -0
- readyagents/config.py +264 -0
- readyagents/errors.py +129 -0
- readyagents/llm/__init__.py +11 -0
- readyagents/llm/anthropic_provider.py +72 -0
- readyagents/llm/base.py +57 -0
- readyagents/llm/cache.py +86 -0
- readyagents/llm/openai_compat.py +12 -0
- readyagents/llm/openai_provider.py +70 -0
- readyagents/llm/registry.py +112 -0
- readyagents/llm/resilience.py +179 -0
- readyagents/llm/tool_calls.py +286 -0
- readyagents/logging.py +162 -0
- readyagents/mcp/__init__.py +43 -0
- readyagents/mcp/builtin.py +674 -0
- readyagents/mcp/client.py +253 -0
- readyagents/mcp/http.py +585 -0
- readyagents/mcp/run_api.py +1077 -0
- readyagents/mcp/server.py +246 -0
- readyagents/notify.py +63 -0
- readyagents/packs/__init__.py +26 -0
- readyagents/packs/loader.py +157 -0
- readyagents/packs/protocol.py +55 -0
- readyagents/policy.py +127 -0
- readyagents/py.typed +1 -0
- readyagents/report.py +88 -0
- readyagents/scaffold.py +410 -0
- readyagents/secrets.py +120 -0
- readyagents/testing/__init__.py +17 -0
- readyagents/testing/eval.py +219 -0
- readyagents/testing/helpers.py +128 -0
- readyagents/testing/recorded.py +68 -0
- readyagents/tools/__init__.py +67 -0
- readyagents/workflow/__init__.py +3 -0
- readyagents/workflow/cancellation.py +88 -0
- readyagents/workflow/conditions.py +279 -0
- readyagents/workflow/engine.py +354 -0
- readyagents/workflow/nodes.py +944 -0
- readyagents/workflow/runner.py +375 -0
- readyagents/workflow/schema.py +287 -0
- readyagents/workflow/state.py +472 -0
- readyagents/workflow/structured.py +103 -0
- readyagents/workflow/templates.py +125 -0
- readyagentsdev-0.8.2.dist-info/METADATA +215 -0
- readyagentsdev-0.8.2.dist-info/RECORD +51 -0
- readyagentsdev-0.8.2.dist-info/WHEEL +4 -0
- readyagentsdev-0.8.2.dist-info/entry_points.txt +2 -0
- readyagentsdev-0.8.2.dist-info/licenses/LICENSE +201 -0
readyagents/report.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Local HTML run reports — no extra dependencies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from readyagents.workflow.state import RunState
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def render_html(state: RunState) -> str:
|
|
12
|
+
rows = []
|
|
13
|
+
for result in state.results:
|
|
14
|
+
preview = result.error or result.output
|
|
15
|
+
rows.append(
|
|
16
|
+
"<tr>"
|
|
17
|
+
f"<td><code>{html.escape(result.node_id)}</code></td>"
|
|
18
|
+
f"<td>{html.escape(result.type)}</td>"
|
|
19
|
+
f"<td class='st-{html.escape(result.status)}'>{html.escape(result.status)}</td>"
|
|
20
|
+
f"<td><pre>{html.escape(_preview(preview))}</pre></td>"
|
|
21
|
+
f"<td>{result.attempts}</td>"
|
|
22
|
+
"</tr>"
|
|
23
|
+
)
|
|
24
|
+
usage = ", ".join(f"{html.escape(k)}={v}" for k, v in state.usage.items()) or "—"
|
|
25
|
+
outputs = html.escape(_preview(state.output_keys or state.node_outputs, 4000))
|
|
26
|
+
pending = html.escape(state.pending_node) if state.pending_node else "—"
|
|
27
|
+
return f"""<!DOCTYPE html>
|
|
28
|
+
<html lang="en">
|
|
29
|
+
<head>
|
|
30
|
+
<meta charset="utf-8"/>
|
|
31
|
+
<title>Run {html.escape(state.run_id)} — {html.escape(state.status)}</title>
|
|
32
|
+
<style>
|
|
33
|
+
body {{ font-family: ui-sans-serif, system-ui, sans-serif; margin: 2rem; color: #111; }}
|
|
34
|
+
h1 {{ font-size: 1.25rem; }}
|
|
35
|
+
.meta {{ display: grid; grid-template-columns: 10rem 1fr; gap: .35rem 1rem; margin: 1rem 0 2rem; }}
|
|
36
|
+
.meta div.k {{ color: #555; }}
|
|
37
|
+
table {{ border-collapse: collapse; width: 100%; }}
|
|
38
|
+
th, td {{ border-bottom: 1px solid #ddd; text-align: left;
|
|
39
|
+
padding: .45rem .4rem; vertical-align: top; }}
|
|
40
|
+
th {{ font-size: .8rem; text-transform: uppercase; color: #555; }}
|
|
41
|
+
pre {{ white-space: pre-wrap; margin: 0; font-size: .85rem; }}
|
|
42
|
+
.st-ok {{ color: #0a7; font-weight: 600; }}
|
|
43
|
+
.st-error {{ color: #c20; font-weight: 600; }}
|
|
44
|
+
.st-paused, .badge-paused {{ color: #b80; }}
|
|
45
|
+
.badge {{ display: inline-block; padding: .15rem .5rem; border-radius: 999px; background: #eee; }}
|
|
46
|
+
.badge-succeeded {{ background: #e6f7f0; color: #0a7; }}
|
|
47
|
+
.badge-failed {{ background: #fdeaea; color: #c20; }}
|
|
48
|
+
.badge-paused {{ background: #fff4d6; }}
|
|
49
|
+
footer {{ margin-top: 2rem; color: #777; font-size: .85rem; }}
|
|
50
|
+
</style>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<h1>ReadyAgents run <code>{html.escape(state.run_id)}</code>
|
|
54
|
+
<span class="badge badge-{html.escape(state.status)}">{html.escape(state.status)}</span></h1>
|
|
55
|
+
<div class="meta">
|
|
56
|
+
<div class="k">workflow</div><div>{html.escape(state.workflow_name)}</div>
|
|
57
|
+
<div class="k">started</div><div>{html.escape(state.started_at)}</div>
|
|
58
|
+
<div class="k">finished</div><div>{html.escape(str(state.finished_at or "—"))}</div>
|
|
59
|
+
<div class="k">pending_node</div><div>{pending}</div>
|
|
60
|
+
<div class="k">usage</div><div>{usage}</div>
|
|
61
|
+
</div>
|
|
62
|
+
<h2>Timeline</h2>
|
|
63
|
+
<table>
|
|
64
|
+
<thead><tr><th>Node</th><th>Type</th><th>Status</th><th>Output</th><th>Attempts</th></tr></thead>
|
|
65
|
+
<tbody>
|
|
66
|
+
{"".join(rows) or '<tr><td colspan="5">No nodes recorded.</td></tr>'}
|
|
67
|
+
</tbody>
|
|
68
|
+
</table>
|
|
69
|
+
<h2>Outputs</h2>
|
|
70
|
+
<pre>{outputs}</pre>
|
|
71
|
+
<footer>Generated by ReadyAgents Core. Local report — not uploaded anywhere.</footer>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def write_html_report(state: RunState, dest: Path) -> Path:
|
|
78
|
+
dest = Path(dest)
|
|
79
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
dest.write_text(render_html(state), encoding="utf-8")
|
|
81
|
+
return dest
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _preview(value: object, limit: int = 800) -> str:
|
|
85
|
+
text = value if isinstance(value, str) else repr(value)
|
|
86
|
+
if len(text) > limit:
|
|
87
|
+
return text[: limit - 1] + "…"
|
|
88
|
+
return text
|
readyagents/scaffold.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""Create a starter ReadyAgents project (workflow + README + .env pattern)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from readyagents.errors import ConfigError
|
|
8
|
+
|
|
9
|
+
TEMPLATES = (
|
|
10
|
+
"basic",
|
|
11
|
+
"approval",
|
|
12
|
+
"research",
|
|
13
|
+
"pipeline",
|
|
14
|
+
"review",
|
|
15
|
+
"foreach",
|
|
16
|
+
"agent-tools",
|
|
17
|
+
"gated",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_ENV = """# ReadyAgents BYOK — fill in your keys. Never commit real keys.
|
|
21
|
+
|
|
22
|
+
READYAGENTS_DEFAULT_MODEL=openai:gpt-4o-mini
|
|
23
|
+
OPENAI_API_KEY=
|
|
24
|
+
ANTHROPIC_API_KEY=
|
|
25
|
+
# READYAGENTS_ALLOW_HTTP=0
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
_WORKFLOWS: dict[str, str] = {
|
|
29
|
+
"basic": """name: {name}
|
|
30
|
+
version: "1"
|
|
31
|
+
description: >
|
|
32
|
+
Basic starter from `readyagents new --template basic`. No API keys.
|
|
33
|
+
Run: readyagents run workflow.yaml
|
|
34
|
+
|
|
35
|
+
start: stamp
|
|
36
|
+
nodes:
|
|
37
|
+
- id: stamp
|
|
38
|
+
type: tool
|
|
39
|
+
tool: now
|
|
40
|
+
output_key: timestamp
|
|
41
|
+
next: greet
|
|
42
|
+
|
|
43
|
+
- id: greet
|
|
44
|
+
type: transform
|
|
45
|
+
template: "{name} ok at {{{{timestamp}}}}"
|
|
46
|
+
output_key: summary
|
|
47
|
+
""",
|
|
48
|
+
"approval": """name: {name}
|
|
49
|
+
version: "1"
|
|
50
|
+
description: >
|
|
51
|
+
Approval starter from `readyagents new --template approval`. No API keys.
|
|
52
|
+
Run: readyagents run workflow.yaml --approve gate
|
|
53
|
+
|
|
54
|
+
start: stamp
|
|
55
|
+
nodes:
|
|
56
|
+
- id: stamp
|
|
57
|
+
type: tool
|
|
58
|
+
tool: now
|
|
59
|
+
output_key: timestamp
|
|
60
|
+
next: greet
|
|
61
|
+
|
|
62
|
+
- id: greet
|
|
63
|
+
type: transform
|
|
64
|
+
template: "{name} ok at {{{{timestamp}}}}"
|
|
65
|
+
output_key: summary
|
|
66
|
+
next: gate
|
|
67
|
+
|
|
68
|
+
- id: gate
|
|
69
|
+
type: approval
|
|
70
|
+
prompt: "Accept starter summary? {{{{summary}}}}"
|
|
71
|
+
then: done
|
|
72
|
+
else: stopped
|
|
73
|
+
|
|
74
|
+
- id: done
|
|
75
|
+
type: transform
|
|
76
|
+
template: "approved: {{{{summary}}}}"
|
|
77
|
+
output_key: result
|
|
78
|
+
|
|
79
|
+
- id: stopped
|
|
80
|
+
type: transform
|
|
81
|
+
template: "rejected: {{{{summary}}}}"
|
|
82
|
+
output_key: result
|
|
83
|
+
""",
|
|
84
|
+
"pipeline": """name: {name}
|
|
85
|
+
version: "1"
|
|
86
|
+
description: >
|
|
87
|
+
Keyless pipeline starter (`--template pipeline`): calc, json_get, condition.
|
|
88
|
+
Run: readyagents run workflow.yaml
|
|
89
|
+
|
|
90
|
+
start: add
|
|
91
|
+
nodes:
|
|
92
|
+
- id: add
|
|
93
|
+
type: tool
|
|
94
|
+
tool: calc
|
|
95
|
+
arguments:
|
|
96
|
+
expression: "6 * 7"
|
|
97
|
+
output_key: n
|
|
98
|
+
next: pack
|
|
99
|
+
|
|
100
|
+
- id: pack
|
|
101
|
+
type: transform
|
|
102
|
+
template: '{{"n": {{{{n}}}}}}'
|
|
103
|
+
output_key: blob
|
|
104
|
+
next: pick
|
|
105
|
+
|
|
106
|
+
- id: pick
|
|
107
|
+
type: tool
|
|
108
|
+
tool: json_get
|
|
109
|
+
arguments:
|
|
110
|
+
data: "{{{{blob}}}}"
|
|
111
|
+
path: n
|
|
112
|
+
output_key: extracted
|
|
113
|
+
next: check
|
|
114
|
+
|
|
115
|
+
- id: check
|
|
116
|
+
type: condition
|
|
117
|
+
when: extracted == 42
|
|
118
|
+
then: ok
|
|
119
|
+
else: bad
|
|
120
|
+
|
|
121
|
+
- id: ok
|
|
122
|
+
type: transform
|
|
123
|
+
template: "{name} pipeline ok: {{{{extracted}}}}"
|
|
124
|
+
output_key: summary
|
|
125
|
+
|
|
126
|
+
- id: bad
|
|
127
|
+
type: transform
|
|
128
|
+
template: "{name} pipeline unexpected: {{{{extracted}}}}"
|
|
129
|
+
output_key: summary
|
|
130
|
+
""",
|
|
131
|
+
"review": """name: {name}
|
|
132
|
+
version: "1"
|
|
133
|
+
description: >
|
|
134
|
+
File-review starter (`--template review`). Reads a workspace file, then a
|
|
135
|
+
transform you can later swap for an agent node. No API keys.
|
|
136
|
+
Run: readyagents run workflow.yaml --input path=README.md
|
|
137
|
+
|
|
138
|
+
inputs:
|
|
139
|
+
path: README.md
|
|
140
|
+
|
|
141
|
+
start: read
|
|
142
|
+
nodes:
|
|
143
|
+
- id: read
|
|
144
|
+
type: tool
|
|
145
|
+
tool: read_file
|
|
146
|
+
arguments:
|
|
147
|
+
path: "{{{{path}}}}"
|
|
148
|
+
output_key: source
|
|
149
|
+
next: note
|
|
150
|
+
|
|
151
|
+
- id: note
|
|
152
|
+
type: transform
|
|
153
|
+
template: "review {{{{path}}}} ({name}): {{{{source}}}}"
|
|
154
|
+
output_key: summary
|
|
155
|
+
next: gate
|
|
156
|
+
|
|
157
|
+
- id: gate
|
|
158
|
+
type: approval
|
|
159
|
+
prompt: "Accept review of {{{{path}}}}?"
|
|
160
|
+
then: done
|
|
161
|
+
else: hold
|
|
162
|
+
|
|
163
|
+
- id: done
|
|
164
|
+
type: transform
|
|
165
|
+
template: "accepted: {{{{path}}}}"
|
|
166
|
+
output_key: result
|
|
167
|
+
|
|
168
|
+
- id: hold
|
|
169
|
+
type: transform
|
|
170
|
+
template: "held: {{{{path}}}}"
|
|
171
|
+
output_key: result
|
|
172
|
+
""",
|
|
173
|
+
"research": """name: {name}
|
|
174
|
+
version: "1"
|
|
175
|
+
description: >
|
|
176
|
+
Research-style starter. Fan-out two builtin tools, then an approval gate.
|
|
177
|
+
No API keys. Run: readyagents run workflow.yaml --approve publish
|
|
178
|
+
|
|
179
|
+
start: fan
|
|
180
|
+
nodes:
|
|
181
|
+
- id: fan
|
|
182
|
+
type: parallel
|
|
183
|
+
output_key: parts
|
|
184
|
+
next: combine
|
|
185
|
+
branches:
|
|
186
|
+
- id: math
|
|
187
|
+
type: tool
|
|
188
|
+
tool: calc
|
|
189
|
+
arguments:
|
|
190
|
+
expression: "21 * 2"
|
|
191
|
+
- id: when
|
|
192
|
+
type: tool
|
|
193
|
+
tool: now
|
|
194
|
+
|
|
195
|
+
- id: combine
|
|
196
|
+
type: transform
|
|
197
|
+
template: "value={{{{parts.math}}}} at {{{{parts.when}}}}"
|
|
198
|
+
output_key: brief
|
|
199
|
+
next: publish
|
|
200
|
+
|
|
201
|
+
- id: publish
|
|
202
|
+
type: approval
|
|
203
|
+
prompt: "Publish brief? {{{{brief}}}}"
|
|
204
|
+
then: ok
|
|
205
|
+
else: hold
|
|
206
|
+
|
|
207
|
+
- id: ok
|
|
208
|
+
type: transform
|
|
209
|
+
template: "{name} published: {{{{brief}}}}"
|
|
210
|
+
output_key: result
|
|
211
|
+
|
|
212
|
+
- id: hold
|
|
213
|
+
type: transform
|
|
214
|
+
template: "{name} held: {{{{brief}}}}"
|
|
215
|
+
output_key: result
|
|
216
|
+
""",
|
|
217
|
+
"foreach": """name: {name}
|
|
218
|
+
version: "1"
|
|
219
|
+
description: >
|
|
220
|
+
Foreach starter from `readyagents new --template foreach`. No API keys.
|
|
221
|
+
Run: readyagents run workflow.yaml
|
|
222
|
+
|
|
223
|
+
inputs:
|
|
224
|
+
expressions:
|
|
225
|
+
- "1+1"
|
|
226
|
+
- "2+2"
|
|
227
|
+
|
|
228
|
+
start: each
|
|
229
|
+
nodes:
|
|
230
|
+
- id: each
|
|
231
|
+
type: foreach
|
|
232
|
+
items: expressions
|
|
233
|
+
max_items: 32
|
|
234
|
+
output_key: results
|
|
235
|
+
body:
|
|
236
|
+
id: math
|
|
237
|
+
type: tool
|
|
238
|
+
tool: calc
|
|
239
|
+
arguments:
|
|
240
|
+
expression: "{{{{item}}}}"
|
|
241
|
+
""",
|
|
242
|
+
"agent-tools": """name: {name}
|
|
243
|
+
version: "1"
|
|
244
|
+
description: >
|
|
245
|
+
Agent tools starter (`--template agent-tools`). Live run needs an API key.
|
|
246
|
+
Keyless dry-run: readyagents run workflow.yaml --dry-run
|
|
247
|
+
|
|
248
|
+
start: worker
|
|
249
|
+
nodes:
|
|
250
|
+
- id: worker
|
|
251
|
+
type: agent
|
|
252
|
+
prompt: |
|
|
253
|
+
Use the calc tool if you need arithmetic.
|
|
254
|
+
What is 2+2? Reply with the number only.
|
|
255
|
+
tools:
|
|
256
|
+
- calc
|
|
257
|
+
max_tool_rounds: 4
|
|
258
|
+
output_key: answer
|
|
259
|
+
timeout_seconds: 60
|
|
260
|
+
""",
|
|
261
|
+
"gated": """name: {name}
|
|
262
|
+
version: "1"
|
|
263
|
+
description: >
|
|
264
|
+
Gated write starter (`--template gated`). Pause does not create the file.
|
|
265
|
+
Pause: readyagents run workflow.yaml
|
|
266
|
+
Resume: readyagents resume <run_id> --approve gate
|
|
267
|
+
One shot: readyagents run workflow.yaml --approve gate
|
|
268
|
+
|
|
269
|
+
start: add
|
|
270
|
+
nodes:
|
|
271
|
+
- id: add
|
|
272
|
+
type: tool
|
|
273
|
+
tool: calc
|
|
274
|
+
arguments:
|
|
275
|
+
expression: "19 + 23"
|
|
276
|
+
output_key: total
|
|
277
|
+
next: gate
|
|
278
|
+
|
|
279
|
+
- id: gate
|
|
280
|
+
type: approval
|
|
281
|
+
prompt: "Write gated.txt with total {{{{total}}}}?"
|
|
282
|
+
then: write
|
|
283
|
+
else: denied
|
|
284
|
+
|
|
285
|
+
- id: write
|
|
286
|
+
type: tool
|
|
287
|
+
tool: write_file
|
|
288
|
+
arguments:
|
|
289
|
+
path: gated.txt
|
|
290
|
+
content: "gated ok: {{{{total}}}}\\n"
|
|
291
|
+
output_key: written
|
|
292
|
+
|
|
293
|
+
- id: denied
|
|
294
|
+
type: transform
|
|
295
|
+
template: "gated denied: {{{{total}}}}"
|
|
296
|
+
output_key: summary
|
|
297
|
+
""",
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
_READMES: dict[str, str] = {
|
|
301
|
+
"basic": """# {name}
|
|
302
|
+
|
|
303
|
+
Basic ReadyAgents starter (`--template basic`). BYOK. No approval gate.
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
readyagents run workflow.yaml
|
|
307
|
+
readyagents runs list
|
|
308
|
+
readyagents runs show <run_id>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Copy `.env.example` to `.env` if you add agent nodes.
|
|
312
|
+
""",
|
|
313
|
+
"approval": """# {name}
|
|
314
|
+
|
|
315
|
+
Approval starter (`--template approval`). BYOK.
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
readyagents run workflow.yaml --approve gate
|
|
319
|
+
# or pause, then:
|
|
320
|
+
readyagents run workflow.yaml
|
|
321
|
+
readyagents resume <run_id> --approve gate
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Copy `.env.example` to `.env` if you add agent nodes.
|
|
325
|
+
""",
|
|
326
|
+
"pipeline": """# {name}
|
|
327
|
+
|
|
328
|
+
Pipeline starter (`--template pipeline`). Builtin tools only.
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
readyagents run workflow.yaml
|
|
332
|
+
readyagents runs show <run_id>
|
|
333
|
+
readyagents runs report <run_id>
|
|
334
|
+
```
|
|
335
|
+
""",
|
|
336
|
+
"review": """# {name}
|
|
337
|
+
|
|
338
|
+
File-review starter (`--template review`). Keyless transform; swap `note` for
|
|
339
|
+
an `agent` node when you add API keys.
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
readyagents run workflow.yaml --input path=README.md --approve gate
|
|
343
|
+
readyagents resume <run_id> --approve gate
|
|
344
|
+
```
|
|
345
|
+
""",
|
|
346
|
+
"research": """# {name}
|
|
347
|
+
|
|
348
|
+
Research-style starter (`--template research`): parallel fan-out + approval.
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
readyagents run workflow.yaml --approve publish
|
|
352
|
+
readyagents run workflow.yaml --dry-run --approve publish
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
No API keys required. Add `type: agent` nodes and keys later.
|
|
356
|
+
""",
|
|
357
|
+
"foreach": """# {name}
|
|
358
|
+
|
|
359
|
+
Foreach starter (`--template foreach`). Bounded list + `calc`. No API keys.
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
readyagents run workflow.yaml
|
|
363
|
+
```
|
|
364
|
+
""",
|
|
365
|
+
"agent-tools": """# {name}
|
|
366
|
+
|
|
367
|
+
Agent tools starter (`--template agent-tools`). Allowlisted `calc`.
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
readyagents run workflow.yaml --dry-run
|
|
371
|
+
# with keys:
|
|
372
|
+
readyagents run workflow.yaml
|
|
373
|
+
```
|
|
374
|
+
""",
|
|
375
|
+
"gated": """# {name}
|
|
376
|
+
|
|
377
|
+
Gated write starter (`--template gated`). Pause (exit 2) does not create the file.
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
readyagents run workflow.yaml
|
|
381
|
+
readyagents resume <run_id> --approve gate
|
|
382
|
+
readyagents run workflow.yaml --approve gate
|
|
383
|
+
```
|
|
384
|
+
""",
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def create_project(dest: Path, *, name: str, template: str = "pipeline") -> list[Path]:
|
|
389
|
+
dest = dest.expanduser().resolve()
|
|
390
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
391
|
+
kind = (template or "pipeline").strip().lower()
|
|
392
|
+
if kind not in _WORKFLOWS:
|
|
393
|
+
raise ConfigError(f"Unknown template '{template}'. Choose one of: {', '.join(TEMPLATES)}")
|
|
394
|
+
workflow = dest / "workflow.yaml"
|
|
395
|
+
readme = dest / "README.md"
|
|
396
|
+
env_example = dest / ".env.example"
|
|
397
|
+
for path in (workflow, readme, env_example):
|
|
398
|
+
if path.exists():
|
|
399
|
+
raise ConfigError(f"Refusing to overwrite existing file: {path}")
|
|
400
|
+
slug = _slug(name)
|
|
401
|
+
workflow.write_text(_WORKFLOWS[kind].format(name=slug), encoding="utf-8")
|
|
402
|
+
readme.write_text(_READMES[kind].format(name=slug), encoding="utf-8")
|
|
403
|
+
env_example.write_text(_ENV, encoding="utf-8")
|
|
404
|
+
return [workflow, readme, env_example]
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _slug(name: str) -> str:
|
|
408
|
+
cleaned = "".join(ch if ch.isalnum() or ch in "-_" else "-" for ch in name.strip())
|
|
409
|
+
cleaned = cleaned.strip("-_") or "starter"
|
|
410
|
+
return cleaned
|
readyagents/secrets.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Secrets-manager hooks. Env / `.env` remains the default BYOK path."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from typing import Any, Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
from readyagents.config import Settings
|
|
9
|
+
from readyagents.errors import LLMError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class SecretsBackend(Protocol):
|
|
14
|
+
"""Resolve a secret by name (usually an env-style key like ``OPENAI_API_KEY``)."""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
|
|
18
|
+
def get(self, key: str) -> str | None: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MappingSecrets:
|
|
22
|
+
"""In-process / pack test backend. Not a vendor SDK."""
|
|
23
|
+
|
|
24
|
+
name = "mapping"
|
|
25
|
+
|
|
26
|
+
def __init__(self, values: Mapping[str, str], *, name: str = "mapping") -> None:
|
|
27
|
+
self.name = name
|
|
28
|
+
self._values = {str(k): str(v) for k, v in values.items() if v is not None}
|
|
29
|
+
|
|
30
|
+
def get(self, key: str) -> str | None:
|
|
31
|
+
value = self._values.get(key)
|
|
32
|
+
if value is None or not str(value).strip():
|
|
33
|
+
return None
|
|
34
|
+
return str(value)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_PROVIDER_KEYS = {
|
|
38
|
+
"openai": ("OPENAI_API_KEY", "READYAGENTS_OPENAI_API_KEY"),
|
|
39
|
+
"anthropic": ("ANTHROPIC_API_KEY", "READYAGENTS_ANTHROPIC_API_KEY"),
|
|
40
|
+
"openai-compat": (
|
|
41
|
+
"OPENAI_COMPAT_API_KEY",
|
|
42
|
+
"READYAGENTS_OPENAI_COMPAT_API_KEY",
|
|
43
|
+
"GROQ_API_KEY",
|
|
44
|
+
"OPENAI_API_KEY",
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def lookup_secret(
|
|
50
|
+
key: str,
|
|
51
|
+
backends: Sequence[SecretsBackend] | SecretsBackend | None,
|
|
52
|
+
) -> str | None:
|
|
53
|
+
if not backends:
|
|
54
|
+
return None
|
|
55
|
+
items = as_backends(backends)
|
|
56
|
+
if not items and isinstance(backends, SecretsBackend):
|
|
57
|
+
items = [backends]
|
|
58
|
+
for backend in items:
|
|
59
|
+
getter = getattr(backend, "get", None)
|
|
60
|
+
if not callable(getter):
|
|
61
|
+
continue
|
|
62
|
+
try:
|
|
63
|
+
value = getter(key)
|
|
64
|
+
except Exception: # noqa: BLE001
|
|
65
|
+
continue
|
|
66
|
+
if value is None:
|
|
67
|
+
continue
|
|
68
|
+
text = str(value).strip()
|
|
69
|
+
if text:
|
|
70
|
+
return text
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def secret_for_provider(
|
|
75
|
+
provider: str,
|
|
76
|
+
*,
|
|
77
|
+
settings: Settings | None = None,
|
|
78
|
+
secrets: Sequence[SecretsBackend] | SecretsBackend | None = None,
|
|
79
|
+
) -> str | None:
|
|
80
|
+
"""Settings/env first (BYOK default), then secrets-manager hooks."""
|
|
81
|
+
if settings is not None:
|
|
82
|
+
from_settings = settings.api_key_for(provider)
|
|
83
|
+
if from_settings:
|
|
84
|
+
return from_settings
|
|
85
|
+
names = _PROVIDER_KEYS.get(provider.lower()) or (provider.upper() + "_API_KEY",)
|
|
86
|
+
for name in names:
|
|
87
|
+
found = lookup_secret(name, secrets)
|
|
88
|
+
if found:
|
|
89
|
+
return found
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def require_secret(
|
|
94
|
+
provider: str,
|
|
95
|
+
*,
|
|
96
|
+
settings: Settings | None = None,
|
|
97
|
+
secrets: Sequence[SecretsBackend] | None = None,
|
|
98
|
+
) -> str:
|
|
99
|
+
"""Like ``require_api_key`` but consults secrets backends after env."""
|
|
100
|
+
from readyagents.config import get_settings, require_api_key
|
|
101
|
+
|
|
102
|
+
settings = settings or get_settings()
|
|
103
|
+
found = secret_for_provider(provider, settings=settings, secrets=secrets)
|
|
104
|
+
if found:
|
|
105
|
+
return found
|
|
106
|
+
# Reuse the BYOK error wording when nothing is configured.
|
|
107
|
+
try:
|
|
108
|
+
return require_api_key(provider, settings)
|
|
109
|
+
except LLMError:
|
|
110
|
+
raise
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def as_backends(raw: Any) -> list[SecretsBackend]:
|
|
114
|
+
if raw is None:
|
|
115
|
+
return []
|
|
116
|
+
if isinstance(raw, SecretsBackend):
|
|
117
|
+
return [raw]
|
|
118
|
+
if isinstance(raw, Sequence) and not isinstance(raw, (str, bytes)):
|
|
119
|
+
return [item for item in raw if isinstance(item, SecretsBackend)]
|
|
120
|
+
return []
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Workflow unit-test helpers, recorded LLM mocks, and a tiny eval harness."""
|
|
2
|
+
|
|
3
|
+
from readyagents.testing.eval import EvalCase, EvalReport, EvalResult, load_eval_suite, run_eval
|
|
4
|
+
from readyagents.testing.helpers import ScriptedLLM, run_workflow_file_test, run_workflow_spec
|
|
5
|
+
from readyagents.testing.recorded import RecordedLLM
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"EvalCase",
|
|
9
|
+
"EvalReport",
|
|
10
|
+
"EvalResult",
|
|
11
|
+
"RecordedLLM",
|
|
12
|
+
"ScriptedLLM",
|
|
13
|
+
"load_eval_suite",
|
|
14
|
+
"run_eval",
|
|
15
|
+
"run_workflow_file_test",
|
|
16
|
+
"run_workflow_spec",
|
|
17
|
+
]
|