specmodule 0.1.0__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.
- llm/__init__.py +21 -0
- llm/client.py +654 -0
- llm/config.py +213 -0
- module_harness/__init__.py +201 -0
- module_harness/align.py +39 -0
- module_harness/builtins.py +29 -0
- module_harness/checkpoint.py +336 -0
- module_harness/cli.py +1492 -0
- module_harness/command.py +115 -0
- module_harness/config.py +95 -0
- module_harness/consistency.py +123 -0
- module_harness/entry.py +74 -0
- module_harness/events.py +149 -0
- module_harness/feed.py +197 -0
- module_harness/graph_builder.py +334 -0
- module_harness/harness.py +181 -0
- module_harness/loader.py +215 -0
- module_harness/module.py +452 -0
- module_harness/outputfmt.py +139 -0
- module_harness/prompt.py +84 -0
- module_harness/query.py +216 -0
- module_harness/registry.py +180 -0
- module_harness/scaffold.py +404 -0
- module_harness/spec.py +209 -0
- module_harness/status.py +96 -0
- module_harness/store.py +482 -0
- module_harness/submodule.py +268 -0
- module_harness/templates/builtin/codereview.json +32 -0
- module_harness/templates/builtin/docwrite.json +30 -0
- module_harness/templates/builtin/summarize.json +24 -0
- module_harness/templates/builtin/translate.json +27 -0
- module_harness/translator.py +314 -0
- specmodule-0.1.0.dist-info/METADATA +321 -0
- specmodule-0.1.0.dist-info/RECORD +38 -0
- specmodule-0.1.0.dist-info/WHEEL +5 -0
- specmodule-0.1.0.dist-info/entry_points.txt +2 -0
- specmodule-0.1.0.dist-info/licenses/LICENSE +21 -0
- specmodule-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# module_harness/scaffold.py
|
|
2
|
+
"""specmodule init 脚手架生成逻辑(纯函数,与 CLI 解析分离)。
|
|
3
|
+
|
|
4
|
+
生成单文件 python 原生模块骨架(modules/<name>.py)+ 项目级文件缺啥补啥
|
|
5
|
+
(幂等)。模板文本以模块级常量存放,可直接单测。CLI 只 import 不重实现。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_module_name(name: str) -> bool:
|
|
20
|
+
"""是否合法模块名:合法 Python 标识符。
|
|
21
|
+
|
|
22
|
+
模块名同时是文件 stem、discover 导入名、--module 选择器、entry.name
|
|
23
|
+
与默认 run_id——四处必须一致,故限制为标识符。
|
|
24
|
+
"""
|
|
25
|
+
return bool(_NAME_RE.match(name or ""))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── 项目文件模板(常量)──────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
CONFIG_JSON: dict[str, Any] = {
|
|
31
|
+
"providers": [
|
|
32
|
+
{
|
|
33
|
+
"name": "openai",
|
|
34
|
+
"sdktype": "openai",
|
|
35
|
+
"base_url": None,
|
|
36
|
+
"api_key_env": "OPENAI_API_KEY",
|
|
37
|
+
"timeout": 120,
|
|
38
|
+
"max_retries": 3,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
"models": [],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ENV_EXAMPLE = """# 复制为 .env 并填入真实密钥(.env 已被 .gitignore 排除,不进版本库)
|
|
45
|
+
# config.json 的 provider.api_key_env 指定了变量名——两者必须对齐。
|
|
46
|
+
OPENAI_API_KEY=
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
GITIGNORE = """__pycache__/
|
|
50
|
+
*.pyc
|
|
51
|
+
.env
|
|
52
|
+
.specmodule/
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
SPEC_EXAMPLE_JSON: dict[str, Any] = {"message": "你好,世界"}
|
|
56
|
+
|
|
57
|
+
README_MD = """# {name}
|
|
58
|
+
|
|
59
|
+
由 `specmodule init {name}` 生成的模块骨架。默认模板回显 spec 的 `message`
|
|
60
|
+
字段(单 script 节点,零 LLM 依赖),`--mock` 即可冒烟。
|
|
61
|
+
|
|
62
|
+
## 立即冒烟(无需 API key)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python -m module_harness.cli run --module {name} --mock
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
输出结尾应含 `回显: {{"message": "你好,世界"}}` 一类的节点摘要。
|
|
69
|
+
|
|
70
|
+
## 真实 LLM 运行
|
|
71
|
+
|
|
72
|
+
1. 复制 `.env.example` 为 `.env`,填入真实密钥
|
|
73
|
+
(`config.json` 的 `provider.api_key_env` 指定了密钥的环境变量名)。
|
|
74
|
+
2. 在 `config.json` 的 `models` 里填入你用的模型名。
|
|
75
|
+
3. 运行:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python -m module_harness.cli run --module {name} --spec '{{"message": "..."}}'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 配置分工
|
|
82
|
+
|
|
83
|
+
- `config.json`:非敏感 provider/model 注册表(连接信息 + `api_key_env`
|
|
84
|
+
指向的**变量名**)。
|
|
85
|
+
- `.env`:密钥实际值(gitignored,不进版本库)。
|
|
86
|
+
- `rules.txt`:框架级输出格式约束(可选)。
|
|
87
|
+
|
|
88
|
+
## 命令
|
|
89
|
+
|
|
90
|
+
| 命令 | 作用 |
|
|
91
|
+
|------|------|
|
|
92
|
+
| `run --module {name} [--spec ...] [--mock]` | 运行模块(三级实时显示 `--verbose 1..3`) |
|
|
93
|
+
| `status [--run-id ...]` | 查询运行状态 |
|
|
94
|
+
| `review [--tick N] [--node ...] [--failed]` | 审阅历史时间线 |
|
|
95
|
+
|
|
96
|
+
`init` 只在文件缺失时补齐项目文件(幂等);已存在的 `config.json`、
|
|
97
|
+
`.gitignore` 等保持原样不被覆盖。`--force` 仅覆盖模块文件
|
|
98
|
+
`modules/{name}.py`。
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
# ── 模块骨架模板(__NAME__ / __DESCRIPTION__ 占位符)────────────────
|
|
102
|
+
|
|
103
|
+
MODULE_TEMPLATE = '''"""{description} — 由 specmodule init 生成的模块骨架。
|
|
104
|
+
|
|
105
|
+
一个 module 一个 py 文件(modules/{name}.py):本文件内声明模块级 ``entry``
|
|
106
|
+
变量,CLI ``specmodule run --module {name}`` 经 discover_modules() 导入。
|
|
107
|
+
|
|
108
|
+
增长路径:模块变大(多模板 / submodule / guard)时,把下方实现拆到独立包
|
|
109
|
+
``{name}/``,本文件留薄入口——参照 example/modules/academic_writer.py 的形态。
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
from __future__ import annotations
|
|
113
|
+
|
|
114
|
+
from typing import Any
|
|
115
|
+
|
|
116
|
+
from module_harness.config import HarnessConfig
|
|
117
|
+
from module_harness.entry import ModuleEntry
|
|
118
|
+
from module_harness.events import EventBus
|
|
119
|
+
from module_harness.outputfmt import OutputFormat
|
|
120
|
+
from module_harness.registry import HarnessRegistry
|
|
121
|
+
|
|
122
|
+
# ── harness 组件:HarnessConfig 数据类常量 ─────────────────────────────
|
|
123
|
+
# prompt_core 中的 {message} 占位符由 tasklist inputs 的 {spec.message} 运行时填充。
|
|
124
|
+
HELLO_HARNESS = HarnessConfig(
|
|
125
|
+
name="hello_llm",
|
|
126
|
+
prompt_core="将以下消息改写为正式的英文:{message}",
|
|
127
|
+
output_format=OutputFormat(type="text"),
|
|
128
|
+
temperature=0.3,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ── script 组件:纯 Python 函数,注册后成为图节点 ──────────────────────
|
|
133
|
+
# 签名固定为 fn(view):view.spec.value 读 spec(仅翻译器上下文),
|
|
134
|
+
# view.<节点名>.value 读上游节点输出(runner 上下文)。
|
|
135
|
+
def echo(view: Any) -> dict[str, Any]:
|
|
136
|
+
"""回显上游 Translate 节点的输出(script 消费节点输出)。"""
|
|
137
|
+
return {"message": view.Translate.value}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ── 模板组件:TasklistTemplate dict(translation + tasklist)──────────
|
|
141
|
+
# translation 是注册在 registry 里的 script 节点,收 view 返回 {Tasks, Flow}。
|
|
142
|
+
def _tl_hello(view: Any) -> dict[str, Any]:
|
|
143
|
+
return {
|
|
144
|
+
"Tasks": {
|
|
145
|
+
"Translate": {
|
|
146
|
+
"type": "harness",
|
|
147
|
+
"harness": "hello_llm",
|
|
148
|
+
"inputs": {"message": "{spec.message}"},
|
|
149
|
+
},
|
|
150
|
+
"Echo": {
|
|
151
|
+
"type": "script",
|
|
152
|
+
"script": "echo",
|
|
153
|
+
"inputs": {"data": "Translate"},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
"Flow": "Translate --> Echo",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
HELLO_TEMPLATE: dict[str, Any] = {
|
|
161
|
+
"name": "hello",
|
|
162
|
+
"description": "harness 读入 spec 的 message → script 回显(--mock 可冒烟)",
|
|
163
|
+
"translation": {"type": "script", "script": "tl_hello"},
|
|
164
|
+
"tasklist": {
|
|
165
|
+
"Tasks": {
|
|
166
|
+
"Translate": {
|
|
167
|
+
"type": "harness",
|
|
168
|
+
"harness": "hello_llm",
|
|
169
|
+
"inputs": {"message": "{spec.message}"},
|
|
170
|
+
},
|
|
171
|
+
"Echo": {
|
|
172
|
+
"type": "script",
|
|
173
|
+
"script": "echo",
|
|
174
|
+
"inputs": {"data": "Translate"},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
"Flow": "Translate --> Echo",
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ── registry 构建:注册本模块全部组件(翻译器 + harness + script)──────
|
|
183
|
+
def _build_registry(
|
|
184
|
+
llm_client: Any, template_name: str, event_bus: EventBus
|
|
185
|
+
) -> HarnessRegistry:
|
|
186
|
+
reg = HarnessRegistry(llm_client=llm_client, event_bus=event_bus)
|
|
187
|
+
reg.harness("hello_llm", HELLO_HARNESS)
|
|
188
|
+
reg.script("echo")(echo)
|
|
189
|
+
reg.script("tl_hello")(_tl_hello)
|
|
190
|
+
return reg
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ── 入口声明:discover_modules() 扫描 modules/*.py 找这个变量 ──────────
|
|
194
|
+
entry = ModuleEntry(
|
|
195
|
+
name="{name}",
|
|
196
|
+
description=__DESCRIPTION__,
|
|
197
|
+
templates={"hello": HELLO_TEMPLATE},
|
|
198
|
+
build_registry=_build_registry,
|
|
199
|
+
default_template="hello",
|
|
200
|
+
default_spec={"message": "你好,世界"},
|
|
201
|
+
spec_schema={"message": "str"},
|
|
202
|
+
review_harness=None, # 固定流程骨架模板;需要一致性审核时改回 "spec_tasklist_review"
|
|
203
|
+
)
|
|
204
|
+
'''
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass
|
|
208
|
+
class ScaffoldResult:
|
|
209
|
+
"""init 结果:本次创建/跳过的文件路径清单。"""
|
|
210
|
+
|
|
211
|
+
created: list[Path] = field(default_factory=list)
|
|
212
|
+
skipped: list[Path] = field(default_factory=list)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _write_if_missing(path: Path, content: str, result: ScaffoldResult) -> None:
|
|
216
|
+
"""缺啥补啥:已存在则跳过(不覆盖),否则写入并记入 created。"""
|
|
217
|
+
if path.exists():
|
|
218
|
+
result.skipped.append(path)
|
|
219
|
+
return
|
|
220
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
path.write_text(content, encoding="utf-8")
|
|
222
|
+
result.created.append(path)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def build_module_source(name: str, description: str = "") -> str:
|
|
226
|
+
"""渲染 modules/<name>.py 源码(name 内插 + description 安全嵌入为字面量)。"""
|
|
227
|
+
desc = description or f"{name} — 脚手架生成的示例模块"
|
|
228
|
+
return MODULE_TEMPLATE.replace("{name}", name).replace(
|
|
229
|
+
"__DESCRIPTION__", json.dumps(desc, ensure_ascii=False)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def scaffold(
|
|
234
|
+
name: str,
|
|
235
|
+
*,
|
|
236
|
+
base_dir: str | Path = ".",
|
|
237
|
+
force: bool = False,
|
|
238
|
+
description: str = "",
|
|
239
|
+
) -> ScaffoldResult:
|
|
240
|
+
"""生成模块骨架 + 项目文件缺啥补啥。
|
|
241
|
+
|
|
242
|
+
- 模块名非法 → ValueError(CLI 据此退出码 1,零文件生成)。
|
|
243
|
+
- 模块文件已存在且未 ``force`` → ValueError。
|
|
244
|
+
- ``force`` 仅覆盖模块文件;项目文件永不覆盖(幂等)。
|
|
245
|
+
"""
|
|
246
|
+
if not validate_module_name(name):
|
|
247
|
+
raise ValueError(
|
|
248
|
+
f"模块名 '{name}' 不是合法 Python 标识符"
|
|
249
|
+
"(须匹配 ^[A-Za-z_][A-Za-z0-9_]*$)"
|
|
250
|
+
)
|
|
251
|
+
base = Path(base_dir)
|
|
252
|
+
result = ScaffoldResult()
|
|
253
|
+
|
|
254
|
+
# 模块文件是本次生成的主角:--force 才覆盖,否则报错。
|
|
255
|
+
module_path = base / "modules" / f"{name}.py"
|
|
256
|
+
if module_path.exists() and not force:
|
|
257
|
+
raise ValueError(f"模块文件已存在: {module_path}(用 --force 覆盖)")
|
|
258
|
+
module_path.parent.mkdir(parents=True, exist_ok=True)
|
|
259
|
+
module_path.write_text(build_module_source(name, description), encoding="utf-8")
|
|
260
|
+
result.created.append(module_path)
|
|
261
|
+
|
|
262
|
+
# 项目文件缺啥补啥(幂等,永不覆盖)。
|
|
263
|
+
_write_if_missing(base / "config.json", json.dumps(CONFIG_JSON, ensure_ascii=False, indent=2) + "\n", result)
|
|
264
|
+
_write_if_missing(base / ".env.example", ENV_EXAMPLE, result)
|
|
265
|
+
_write_if_missing(base / ".gitignore", GITIGNORE, result)
|
|
266
|
+
_write_if_missing(
|
|
267
|
+
base / "spec.example.json",
|
|
268
|
+
json.dumps(SPEC_EXAMPLE_JSON, ensure_ascii=False, indent=2) + "\n",
|
|
269
|
+
result,
|
|
270
|
+
)
|
|
271
|
+
_write_if_missing(
|
|
272
|
+
base / "README.md",
|
|
273
|
+
README_MD.replace("{name}", name),
|
|
274
|
+
result,
|
|
275
|
+
)
|
|
276
|
+
return result
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ── 目录形态(init --dir):与已装模块同构的 pack 目录骨架 ─────────────
|
|
280
|
+
|
|
281
|
+
DIR_MODULE_JSON: dict[str, Any] = {
|
|
282
|
+
"name": "__NAME__",
|
|
283
|
+
"version": "0.1.0",
|
|
284
|
+
"description": "__DESCRIPTION__",
|
|
285
|
+
"submodule": True,
|
|
286
|
+
"spec_schema": {"input": {"message": "str"}, "output": {"message": "str"}},
|
|
287
|
+
"requires": [],
|
|
288
|
+
"modules": [],
|
|
289
|
+
"tasklist": {
|
|
290
|
+
"Tasks": {
|
|
291
|
+
"Greet": {
|
|
292
|
+
"type": "script",
|
|
293
|
+
"script": "greet",
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
"Flow": "[Greet]",
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
DIR_SCRIPT_TEMPLATE = '''\
|
|
301
|
+
"""{name} 脚本组件:纯 Python 函数,注册后成为图节点。"""
|
|
302
|
+
|
|
303
|
+
from __future__ import annotations
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def greet(view):
|
|
307
|
+
"""回显上游输入(tasklist 固定:无上游依赖,直接输出)。"""
|
|
308
|
+
return {{"message": "hello from {name}"}}
|
|
309
|
+
'''
|
|
310
|
+
|
|
311
|
+
DIR_HARNESS_EXAMPLE = '''\
|
|
312
|
+
"""{name} harness 组件示例(JSON 文件):LLM 调用节点,三层 prompt。
|
|
313
|
+
|
|
314
|
+
复制为 ``harnesses/<名>.json`` 并在 module.json 的 tasklist 中引用:
|
|
315
|
+
{{
|
|
316
|
+
"type": "harness",
|
|
317
|
+
"harness": "<名>",
|
|
318
|
+
"inputs": {{"text": "{{{{spec.message}}}}"}},
|
|
319
|
+
"outputformat": {{"type": "text"}}
|
|
320
|
+
}}
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
'''
|
|
324
|
+
|
|
325
|
+
DIR_README = """# {name}
|
|
326
|
+
|
|
327
|
+
由 `specmodule init --dir {name}` 生成的目录形态模块骨架。
|
|
328
|
+
|
|
329
|
+
与已装模块同构(pack 格式):`module.json` 声明 spec_schema/tasklist,
|
|
330
|
+
`scripts/` 放 Python 函数,`harnesses/` 放 LLM 调用配置(JSON),
|
|
331
|
+
`commands/` 放 shell 命令配置。
|
|
332
|
+
|
|
333
|
+
运行:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
specmodule run --module {name} --spec '{{"message": "hi"}}' --mock
|
|
337
|
+
specmodule publish {name} --from . --dir # 或直接 install 本目录
|
|
338
|
+
```
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def scaffold_dir(
|
|
343
|
+
name: str,
|
|
344
|
+
*,
|
|
345
|
+
base_dir: str | Path = ".",
|
|
346
|
+
force: bool = False,
|
|
347
|
+
description: str = "",
|
|
348
|
+
) -> ScaffoldResult:
|
|
349
|
+
"""生成目录形态模块骨架(--dir):pack 同构目录 + 项目文件缺啥补啥。
|
|
350
|
+
|
|
351
|
+
与 ``scaffold``(单文件)同语义:模块名非法 → ValueError;模块目录
|
|
352
|
+
已存在且未 force → ValueError;force 仅覆盖模块目录,项目文件永不覆盖。
|
|
353
|
+
"""
|
|
354
|
+
if not validate_module_name(name):
|
|
355
|
+
raise ValueError(
|
|
356
|
+
f"模块名 '{name}' 不是合法 Python 标识符"
|
|
357
|
+
"(须匹配 ^[A-Za-z_][A-Za-z0-9_]*$)"
|
|
358
|
+
)
|
|
359
|
+
base = Path(base_dir)
|
|
360
|
+
result = ScaffoldResult()
|
|
361
|
+
mod_dir = base / "modules" / name
|
|
362
|
+
if mod_dir.exists() and not force:
|
|
363
|
+
raise ValueError(f"模块目录已存在: {mod_dir}(用 --force 覆盖)")
|
|
364
|
+
mod_dir.mkdir(parents=True, exist_ok=True)
|
|
365
|
+
result.created.append(mod_dir)
|
|
366
|
+
|
|
367
|
+
manifest = dict(DIR_MODULE_JSON)
|
|
368
|
+
manifest["name"] = name
|
|
369
|
+
manifest["description"] = description or f"{name} — 脚手架生成的示例模块"
|
|
370
|
+
_write_if_missing(
|
|
371
|
+
mod_dir / "module.json",
|
|
372
|
+
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
|
|
373
|
+
result,
|
|
374
|
+
)
|
|
375
|
+
for sub in ("scripts", "harnesses", "commands", "guards", "submodules"):
|
|
376
|
+
d = mod_dir / sub
|
|
377
|
+
d.mkdir(exist_ok=True)
|
|
378
|
+
result.created.append(d)
|
|
379
|
+
_write_if_missing(
|
|
380
|
+
mod_dir / "scripts" / "greet.py",
|
|
381
|
+
DIR_SCRIPT_TEMPLATE.replace("{name}", name),
|
|
382
|
+
result,
|
|
383
|
+
)
|
|
384
|
+
_write_if_missing(
|
|
385
|
+
mod_dir / "harnesses" / "README.txt",
|
|
386
|
+
DIR_HARNESS_EXAMPLE.replace("{name}", name),
|
|
387
|
+
result,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# 项目文件缺啥补啥(幂等,永不覆盖)——与单文件形态共用。
|
|
391
|
+
_write_if_missing(base / "config.json", json.dumps(CONFIG_JSON, ensure_ascii=False, indent=2) + "\n", result)
|
|
392
|
+
_write_if_missing(base / ".env.example", ENV_EXAMPLE, result)
|
|
393
|
+
_write_if_missing(base / ".gitignore", GITIGNORE, result)
|
|
394
|
+
_write_if_missing(
|
|
395
|
+
base / "spec.example.json",
|
|
396
|
+
json.dumps(SPEC_EXAMPLE_JSON, ensure_ascii=False, indent=2) + "\n",
|
|
397
|
+
result,
|
|
398
|
+
)
|
|
399
|
+
_write_if_missing(
|
|
400
|
+
base / "README.md",
|
|
401
|
+
DIR_README.replace("{name}", name),
|
|
402
|
+
result,
|
|
403
|
+
)
|
|
404
|
+
return result
|
module_harness/spec.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# module_harness/spec.py
|
|
2
|
+
"""Spec 与 Tasklist 数据模型。"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import dataclasses
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Spec:
|
|
12
|
+
"""结构化 spec,用户自由定义字段的键值对集合。"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, data: dict[str, Any]) -> None:
|
|
15
|
+
self._data = dict(data)
|
|
16
|
+
|
|
17
|
+
def __getitem__(self, key: str) -> Any:
|
|
18
|
+
return self._data[key]
|
|
19
|
+
|
|
20
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
21
|
+
return self._data.get(key, default)
|
|
22
|
+
|
|
23
|
+
def keys(self):
|
|
24
|
+
return self._data.keys()
|
|
25
|
+
|
|
26
|
+
def values(self):
|
|
27
|
+
return self._data.values()
|
|
28
|
+
|
|
29
|
+
def items(self):
|
|
30
|
+
return self._data.items()
|
|
31
|
+
|
|
32
|
+
def __len__(self) -> int:
|
|
33
|
+
return len(self._data)
|
|
34
|
+
|
|
35
|
+
def __contains__(self, key: str) -> bool:
|
|
36
|
+
return key in self._data
|
|
37
|
+
|
|
38
|
+
def __repr__(self) -> str:
|
|
39
|
+
return f"Spec({self._data!r})"
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict[str, Any]:
|
|
42
|
+
return dict(self._data)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class TaskDefinition:
|
|
47
|
+
"""tasklist 中单个 Task 的定义。与 HarnessConfig 字段对齐。"""
|
|
48
|
+
|
|
49
|
+
type: Literal["harness", "script", "command", "submodule"]
|
|
50
|
+
harness: str | None = None
|
|
51
|
+
script: str | None = None
|
|
52
|
+
command: str | None = None # type="command" 时引用的命令名
|
|
53
|
+
submodule: str | None = None # type="submodule" 时引用名(父模块 modules 解析)
|
|
54
|
+
outputs: dict[str, str] | None = None # submodule 输出映射 {节点字段: 子输出字段};缺省 = 全量
|
|
55
|
+
timeout: float | None = None # command 超时覆盖(秒)
|
|
56
|
+
cwd: str | None = None # command 工作目录覆盖
|
|
57
|
+
promptmode: str | None = None
|
|
58
|
+
prompt: str | None = None
|
|
59
|
+
outputformat: dict[str, Any] | None = None
|
|
60
|
+
notdo: list[str] | None = None
|
|
61
|
+
model: str | None = None
|
|
62
|
+
temperature: float | None = None
|
|
63
|
+
think: bool | dict | None = None
|
|
64
|
+
api_params: dict[str, Any] | None = None # 透传给 LLM SDK 的额外参数
|
|
65
|
+
inputs: dict[str, str] | None = None
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_dict(cls, d: dict[str, Any]) -> "TaskDefinition":
|
|
69
|
+
return cls(
|
|
70
|
+
type=d["type"],
|
|
71
|
+
harness=d.get("harness"),
|
|
72
|
+
script=d.get("script"),
|
|
73
|
+
command=d.get("command"),
|
|
74
|
+
submodule=d.get("submodule"),
|
|
75
|
+
outputs=d.get("outputs"),
|
|
76
|
+
timeout=d.get("timeout"),
|
|
77
|
+
cwd=d.get("cwd"),
|
|
78
|
+
promptmode=d.get("promptmode"),
|
|
79
|
+
prompt=d.get("prompt"),
|
|
80
|
+
outputformat=d.get("outputformat"),
|
|
81
|
+
notdo=d.get("notdo"),
|
|
82
|
+
model=d.get("model"),
|
|
83
|
+
temperature=d.get("temperature"),
|
|
84
|
+
think=d.get("think"),
|
|
85
|
+
api_params=d.get("api_params"),
|
|
86
|
+
inputs=d.get("inputs"),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class Tasklist:
|
|
92
|
+
"""完整的 tasklist:Tasks + Flow。"""
|
|
93
|
+
|
|
94
|
+
tasks: dict[str, TaskDefinition]
|
|
95
|
+
flow: str
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_json(cls, data: dict[str, Any]) -> "Tasklist":
|
|
99
|
+
if "Tasks" not in data:
|
|
100
|
+
raise ValueError("tasklist 缺少 'Tasks' 字段")
|
|
101
|
+
if "Flow" not in data:
|
|
102
|
+
raise ValueError("tasklist 缺少 'Flow' 字段")
|
|
103
|
+
tasks = {
|
|
104
|
+
key: TaskDefinition.from_dict(td)
|
|
105
|
+
for key, td in data["Tasks"].items()
|
|
106
|
+
}
|
|
107
|
+
return cls(tasks=tasks, flow=data["Flow"])
|
|
108
|
+
|
|
109
|
+
def to_dict(self) -> dict[str, Any]:
|
|
110
|
+
"""JSON 可序列化 dict(与 ``from_json`` 对称)——唯一实现(S4)。"""
|
|
111
|
+
return {
|
|
112
|
+
"Tasks": {k: dataclasses.asdict(v) for k, v in self.tasks.items()},
|
|
113
|
+
"Flow": self.flow,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class TranslationSpec:
|
|
119
|
+
"""翻译方式声明。"""
|
|
120
|
+
|
|
121
|
+
type: Literal["harness", "script"]
|
|
122
|
+
harness: str | None = None
|
|
123
|
+
script: str | None = None
|
|
124
|
+
prompt: str | None = None
|
|
125
|
+
prompt_core: str | None = None
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def from_dict(cls, d: dict[str, Any]) -> "TranslationSpec":
|
|
129
|
+
return cls(
|
|
130
|
+
type=d["type"],
|
|
131
|
+
harness=d.get("harness"),
|
|
132
|
+
script=d.get("script"),
|
|
133
|
+
prompt=d.get("prompt"),
|
|
134
|
+
prompt_core=d.get("prompt_core"),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class TasklistTemplate:
|
|
140
|
+
"""tasklist 模板 = 翻译声明 + tasklist 骨架。"""
|
|
141
|
+
|
|
142
|
+
name: str
|
|
143
|
+
description: str
|
|
144
|
+
translation: TranslationSpec
|
|
145
|
+
tasklist: Tasklist
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def from_json(cls, data: dict[str, Any]) -> "TasklistTemplate":
|
|
149
|
+
if "name" not in data:
|
|
150
|
+
raise ValueError("模板缺少 'name' 字段")
|
|
151
|
+
return cls(
|
|
152
|
+
name=data["name"],
|
|
153
|
+
description=data.get("description", ""),
|
|
154
|
+
translation=TranslationSpec.from_dict(data["translation"]),
|
|
155
|
+
tasklist=Tasklist.from_json(data["tasklist"]),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
_SCHEMA_TYPES: dict[str, type] = {
|
|
160
|
+
"str": str, "int": int, "float": float, "bool": bool,
|
|
161
|
+
"list": list, "dict": dict,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _value_matches(value: Any, type_name: str) -> bool:
|
|
166
|
+
"""判断值是否满足类型声明。bool 与 int 严格区分。"""
|
|
167
|
+
if type_name == "any":
|
|
168
|
+
return True
|
|
169
|
+
expected = _SCHEMA_TYPES.get(type_name)
|
|
170
|
+
if expected is None:
|
|
171
|
+
return False
|
|
172
|
+
if expected is bool:
|
|
173
|
+
return isinstance(value, bool)
|
|
174
|
+
if expected is int:
|
|
175
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
176
|
+
return isinstance(value, expected)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class SpecSchema:
|
|
181
|
+
"""submodule 的 spec 契约:input 校验,output 仅声明。"""
|
|
182
|
+
|
|
183
|
+
input: dict[str, str] = field(default_factory=dict)
|
|
184
|
+
output: dict[str, str] = field(default_factory=dict)
|
|
185
|
+
|
|
186
|
+
def validate(self, spec: dict[str, Any]) -> list[str]:
|
|
187
|
+
"""校验 spec 是否满足契约。返回错误列表,空 = 通过。
|
|
188
|
+
|
|
189
|
+
声明的字段必须存在且类型匹配;未声明的字段允许存在。
|
|
190
|
+
"""
|
|
191
|
+
errors: list[str] = []
|
|
192
|
+
for field_name, type_name in self.input.items():
|
|
193
|
+
if field_name not in spec:
|
|
194
|
+
errors.append(f"缺少字段 '{field_name}'(应为 {type_name})")
|
|
195
|
+
continue
|
|
196
|
+
if not _value_matches(spec[field_name], type_name):
|
|
197
|
+
errors.append(
|
|
198
|
+
f"字段 '{field_name}' 类型错误:期望 {type_name},"
|
|
199
|
+
f"实际 {type(spec[field_name]).__name__}"
|
|
200
|
+
)
|
|
201
|
+
return errors
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class SpecValidationError(Exception):
|
|
205
|
+
"""spec 不满足 spec_schema 契约。"""
|
|
206
|
+
|
|
207
|
+
def __init__(self, errors: list[str]) -> None:
|
|
208
|
+
self.errors = errors
|
|
209
|
+
super().__init__("spec 校验失败:\n" + "\n".join(f" - {e}" for e in errors))
|