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,181 @@
|
|
|
1
|
+
"""Harness 类 — 配置持有 + async body 生成。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from tickflow import Failure
|
|
10
|
+
from tickflow.views import DictView, Missing
|
|
11
|
+
|
|
12
|
+
from .config import HarnessConfig
|
|
13
|
+
from .prompt import PromptRenderer
|
|
14
|
+
from .outputfmt import OutputValidator
|
|
15
|
+
from .events import (
|
|
16
|
+
EventBus,
|
|
17
|
+
PromptRendered,
|
|
18
|
+
LlmCallStarted,
|
|
19
|
+
LlmToken,
|
|
20
|
+
LlmCallCompleted,
|
|
21
|
+
OutputValidated,
|
|
22
|
+
HarnessFailed,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Harness:
|
|
27
|
+
"""持有 HarnessConfig + LLM 客户端 + EventBus。
|
|
28
|
+
|
|
29
|
+
由 HarnessRegistry 管理,用户不直接使用。
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
config: HarnessConfig,
|
|
35
|
+
llm_client: Any,
|
|
36
|
+
event_bus: EventBus,
|
|
37
|
+
) -> None:
|
|
38
|
+
self.config = config
|
|
39
|
+
self.llm = llm_client
|
|
40
|
+
self.bus = event_bus
|
|
41
|
+
self._renderer = PromptRenderer(config)
|
|
42
|
+
|
|
43
|
+
def build_body(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
promptmode: str | None = None,
|
|
47
|
+
prompt_extra: str | None = None,
|
|
48
|
+
spec_inputs: dict[str, Any] | None = None,
|
|
49
|
+
input_aliases: dict[str, str] | None = None,
|
|
50
|
+
):
|
|
51
|
+
"""返回一个 async body callable。
|
|
52
|
+
|
|
53
|
+
``spec_inputs``:spec 字段常量({field_name: value}),
|
|
54
|
+
渲染时作为占位符兜底值(graph_builder 解析 ``{spec.xxx}`` 后注入)。
|
|
55
|
+
|
|
56
|
+
``input_aliases``:跨节点输入别名({field_name: producer})。
|
|
57
|
+
task.inputs 的 field 名在 view 中解析为 Missing(field 不是节点名),
|
|
58
|
+
运行时把 producer 的实际值合并进 extra_values,使 prompt 的
|
|
59
|
+
``{field}`` 占位符能渲染(与 {spec.xxx} 兜底同一机制)。
|
|
60
|
+
|
|
61
|
+
body 执行流程:
|
|
62
|
+
1. 渲染三层 prompt
|
|
63
|
+
2. 调 LLM(流式 token 经 on_token 发射)
|
|
64
|
+
3. 校验输出格式
|
|
65
|
+
4. 发事件
|
|
66
|
+
"""
|
|
67
|
+
config = self.config
|
|
68
|
+
llm = self.llm
|
|
69
|
+
bus = self.bus
|
|
70
|
+
renderer = self._renderer
|
|
71
|
+
validator = OutputValidator(config.output_format) if config.output_format else None
|
|
72
|
+
|
|
73
|
+
async def body(view: DictView) -> Any:
|
|
74
|
+
node = view.node
|
|
75
|
+
now = time.monotonic()
|
|
76
|
+
# view.state 可为 None(bare DictView);写入后自动进入 NodeState.mutable_state 审计
|
|
77
|
+
state = view.state
|
|
78
|
+
|
|
79
|
+
# 1. 渲染 prompt
|
|
80
|
+
extra = dict(spec_inputs) if spec_inputs else {}
|
|
81
|
+
if input_aliases:
|
|
82
|
+
for field, producer in input_aliases.items():
|
|
83
|
+
if field in extra:
|
|
84
|
+
continue
|
|
85
|
+
try:
|
|
86
|
+
val = view[producer].value
|
|
87
|
+
except (KeyError, AttributeError):
|
|
88
|
+
continue
|
|
89
|
+
if val is not Missing:
|
|
90
|
+
extra[field] = val
|
|
91
|
+
rendered = renderer.render(
|
|
92
|
+
view,
|
|
93
|
+
promptmode=promptmode,
|
|
94
|
+
prompt_extra=prompt_extra,
|
|
95
|
+
extra_values=extra,
|
|
96
|
+
)
|
|
97
|
+
if state is not None:
|
|
98
|
+
state["_prompt"] = rendered
|
|
99
|
+
bus.emit(PromptRendered(
|
|
100
|
+
timestamp=time.monotonic(), node=node, tick=0,
|
|
101
|
+
rendered=rendered,
|
|
102
|
+
))
|
|
103
|
+
|
|
104
|
+
# 2. 调用 LLM
|
|
105
|
+
bus.emit(LlmCallStarted(
|
|
106
|
+
timestamp=time.monotonic(), node=node, tick=0,
|
|
107
|
+
model=config.model or "default",
|
|
108
|
+
prompt_chars=len(rendered),
|
|
109
|
+
))
|
|
110
|
+
|
|
111
|
+
def on_token(chunk: str) -> None:
|
|
112
|
+
bus.emit(LlmToken(
|
|
113
|
+
timestamp=time.monotonic(), node=node, tick=0,
|
|
114
|
+
chunk=chunk,
|
|
115
|
+
))
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
from llm.client import LLMError
|
|
119
|
+
|
|
120
|
+
# notdo 由 LLM client 内部通过 _build_system() 拼入 system prompt
|
|
121
|
+
response = await llm.complete(
|
|
122
|
+
prompt=rendered,
|
|
123
|
+
model=config.model,
|
|
124
|
+
temperature=config.temperature,
|
|
125
|
+
think=config.think,
|
|
126
|
+
output_format=dataclasses.asdict(config.output_format) if config.output_format else None,
|
|
127
|
+
notdo=config.notdo if config.notdo else None,
|
|
128
|
+
on_token=on_token,
|
|
129
|
+
api_params=config.api_params if config.api_params else None,
|
|
130
|
+
)
|
|
131
|
+
except LLMError as e:
|
|
132
|
+
if state is not None:
|
|
133
|
+
state["_llm_error"] = str(e)
|
|
134
|
+
bus.emit(HarnessFailed(
|
|
135
|
+
timestamp=time.monotonic(), node=node, tick=0,
|
|
136
|
+
reason=str(e),
|
|
137
|
+
failure_type="infrastructure",
|
|
138
|
+
))
|
|
139
|
+
return Failure(str(e), type="infrastructure")
|
|
140
|
+
|
|
141
|
+
# 3. LLM 原始响应 + usage 写入节点状态(审计链:NodeState.mutable_state)
|
|
142
|
+
if state is not None:
|
|
143
|
+
state["_llm_raw"] = response.content
|
|
144
|
+
state["_usage"] = dict(response.usage)
|
|
145
|
+
|
|
146
|
+
# 3. 校验输出
|
|
147
|
+
bus.emit(LlmCallCompleted(
|
|
148
|
+
timestamp=time.monotonic(), node=node, tick=0,
|
|
149
|
+
content_chars=len(response.content),
|
|
150
|
+
usage=response.usage,
|
|
151
|
+
finish_reason=response.finish_reason,
|
|
152
|
+
))
|
|
153
|
+
|
|
154
|
+
if validator is not None:
|
|
155
|
+
result = validator.validate(response.content)
|
|
156
|
+
if isinstance(result, Failure):
|
|
157
|
+
bus.emit(OutputValidated(
|
|
158
|
+
timestamp=time.monotonic(), node=node, tick=0,
|
|
159
|
+
passed=False,
|
|
160
|
+
extracted=False,
|
|
161
|
+
error=result.error,
|
|
162
|
+
))
|
|
163
|
+
return result
|
|
164
|
+
bus.emit(OutputValidated(
|
|
165
|
+
timestamp=time.monotonic(), node=node, tick=0,
|
|
166
|
+
passed=True,
|
|
167
|
+
extracted=_was_extracted(response.content, result),
|
|
168
|
+
error=None,
|
|
169
|
+
))
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
return response.content
|
|
173
|
+
|
|
174
|
+
return body
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _was_extracted(raw: str, result: Any) -> bool:
|
|
178
|
+
"""简单判断原始内容是否经过了提取处理(内容不直接相等)。"""
|
|
179
|
+
if not isinstance(result, str):
|
|
180
|
+
return True # JSON 解析必然是提取
|
|
181
|
+
return raw.strip() != result.strip()
|
module_harness/loader.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# module_harness/loader.py
|
|
2
|
+
"""ModuleLoader — 加载发布目录为 SubModule 实例(第二层用户入口)。"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from llm import LLMConfig, create_llm_client
|
|
11
|
+
|
|
12
|
+
from .builtins import BUILTIN_HARNESS_NAMES
|
|
13
|
+
from .command import CommandConfig
|
|
14
|
+
from .config import HarnessConfig
|
|
15
|
+
from .events import EventBus
|
|
16
|
+
from .spec import SpecSchema, Tasklist
|
|
17
|
+
from .submodule import SubModule
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ModuleRequirementError(Exception):
|
|
21
|
+
"""requires 声明的名字无法在「内置集 ∪ provides」中解析。"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, missing: list[str], available: list[str]) -> None:
|
|
24
|
+
self.missing = missing
|
|
25
|
+
self.available = available
|
|
26
|
+
super().__init__(
|
|
27
|
+
"requires 无法解析: " + ", ".join(missing)
|
|
28
|
+
+ "\n可用: " + ", ".join(available)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ModuleManifestError(Exception):
|
|
33
|
+
"""module.json 缺失、损坏或缺少必需字段。"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ModuleLoader:
|
|
37
|
+
"""第二层用户入口:加载发布目录,返回可运行的 SubModule 实例。
|
|
38
|
+
|
|
39
|
+
llm_client 优先(测试/注入用);否则由 llm_config(None → from_env)创建。
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
llm_config: LLMConfig | None = None,
|
|
45
|
+
*,
|
|
46
|
+
llm_client: Any = None,
|
|
47
|
+
event_bus: EventBus | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
self._llm_client = llm_client
|
|
50
|
+
self._llm_config = llm_config
|
|
51
|
+
self._event_bus = event_bus
|
|
52
|
+
|
|
53
|
+
def _ensure_client(self) -> Any:
|
|
54
|
+
"""llm_client 优先;否则由 llm_config(None → from_env)惰性创建。"""
|
|
55
|
+
if self._llm_client is None:
|
|
56
|
+
if self._llm_config is None:
|
|
57
|
+
self._llm_config = LLMConfig.from_env()
|
|
58
|
+
self._llm_client = create_llm_client(self._llm_config)
|
|
59
|
+
return self._llm_client
|
|
60
|
+
|
|
61
|
+
def load(self, path: str | Path, *, lazy_client: bool = False) -> SubModule:
|
|
62
|
+
"""解析 module.json → 注册 provides → 校验 requires → 返回 SubModule。
|
|
63
|
+
|
|
64
|
+
``lazy_client=True``:校验/解析阶段不实例化 LLM client(D6——校验
|
|
65
|
+
无网络/key 需求);SubModule 构造传 None,运行期惰性 from_env 或
|
|
66
|
+
由调用方经 ``run(llm_client=...)`` 注入。默认 False 保持旧语义。
|
|
67
|
+
"""
|
|
68
|
+
p = Path(path)
|
|
69
|
+
manifest_path = p / "module.json"
|
|
70
|
+
if not manifest_path.is_file():
|
|
71
|
+
raise ModuleManifestError(f"缺少 module.json: {manifest_path}")
|
|
72
|
+
try:
|
|
73
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
74
|
+
except json.JSONDecodeError as e:
|
|
75
|
+
raise ModuleManifestError(f"module.json 解析失败: {e}") from e
|
|
76
|
+
if not isinstance(manifest, dict):
|
|
77
|
+
raise ModuleManifestError("module.json 顶层必须是对象")
|
|
78
|
+
|
|
79
|
+
name = manifest.get("name")
|
|
80
|
+
tasklist_data = manifest.get("tasklist")
|
|
81
|
+
if not name:
|
|
82
|
+
raise ModuleManifestError("module.json 缺少 'name'")
|
|
83
|
+
if tasklist_data is None:
|
|
84
|
+
raise ModuleManifestError("module.json 缺少 'tasklist'")
|
|
85
|
+
try:
|
|
86
|
+
tasklist = Tasklist.from_json(tasklist_data)
|
|
87
|
+
except (ValueError, TypeError) as e:
|
|
88
|
+
raise ModuleManifestError(f"tasklist 无效: {e}") from e
|
|
89
|
+
|
|
90
|
+
harnesses = self._load_harnesses(p)
|
|
91
|
+
commands = self._load_commands(p)
|
|
92
|
+
scripts = self._load_scripts(p)
|
|
93
|
+
guards = self._load_guards(p)
|
|
94
|
+
submodules = self._load_submodules(p)
|
|
95
|
+
|
|
96
|
+
modules_raw = manifest.get("modules", []) or []
|
|
97
|
+
if not isinstance(modules_raw, list) or not all(
|
|
98
|
+
isinstance(m, str) for m in modules_raw):
|
|
99
|
+
raise ModuleManifestError("modules 必须是字符串列表")
|
|
100
|
+
missing_mods = [m for m in modules_raw if m not in submodules]
|
|
101
|
+
if missing_mods:
|
|
102
|
+
raise ModuleManifestError(
|
|
103
|
+
"modules 缺少子模块目录: " + ", ".join(missing_mods))
|
|
104
|
+
extra_mods = [m for m in submodules if m not in modules_raw]
|
|
105
|
+
if extra_mods:
|
|
106
|
+
raise ModuleManifestError(
|
|
107
|
+
"submodules/ 目录未在 manifest 声明: " + ", ".join(sorted(extra_mods)))
|
|
108
|
+
|
|
109
|
+
schema_data = manifest.get("spec_schema", {}) or {}
|
|
110
|
+
spec_schema = SpecSchema(
|
|
111
|
+
input=schema_data.get("input", {}) or {},
|
|
112
|
+
output=schema_data.get("output", {}) or {},
|
|
113
|
+
)
|
|
114
|
+
requires_raw = manifest.get("requires", []) or []
|
|
115
|
+
if not isinstance(requires_raw, list) or not all(
|
|
116
|
+
isinstance(r, str) for r in requires_raw):
|
|
117
|
+
raise ModuleManifestError("requires 必须是字符串列表")
|
|
118
|
+
requires = list(requires_raw)
|
|
119
|
+
|
|
120
|
+
provides = {h.name for h in harnesses} | {c.name for c in commands} | set(scripts)
|
|
121
|
+
all_names = [h.name for h in harnesses] + [c.name for c in commands] + list(scripts)
|
|
122
|
+
dups = {n for n in all_names if all_names.count(n) > 1}
|
|
123
|
+
if dups:
|
|
124
|
+
raise ModuleManifestError("provides 名称重复: " + ", ".join(sorted(dups)))
|
|
125
|
+
missing = [r for r in requires if r not in BUILTIN_HARNESS_NAMES and r not in provides]
|
|
126
|
+
if missing:
|
|
127
|
+
raise ModuleRequirementError(missing, sorted(BUILTIN_HARNESS_NAMES | provides))
|
|
128
|
+
|
|
129
|
+
cls = type(name, (SubModule,), {
|
|
130
|
+
"name": name,
|
|
131
|
+
"version": manifest.get("version", "0.1.0"),
|
|
132
|
+
"description": manifest.get("description", ""),
|
|
133
|
+
"spec_schema": spec_schema,
|
|
134
|
+
"requires": requires,
|
|
135
|
+
"tasklist": tasklist,
|
|
136
|
+
"harnesses": harnesses,
|
|
137
|
+
"commands": commands,
|
|
138
|
+
"_scripts": scripts,
|
|
139
|
+
"guards": list(guards.items()),
|
|
140
|
+
"modules": submodules,
|
|
141
|
+
})
|
|
142
|
+
client = None if lazy_client else self._ensure_client()
|
|
143
|
+
return cls(llm_client=client, event_bus=self._event_bus)
|
|
144
|
+
|
|
145
|
+
def _load_harnesses(self, p: Path) -> list[HarnessConfig]:
|
|
146
|
+
result: list[HarnessConfig] = []
|
|
147
|
+
for f in sorted((p / "harnesses").glob("*.json")):
|
|
148
|
+
try:
|
|
149
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
150
|
+
cfg = HarnessConfig.from_dict(data)
|
|
151
|
+
except (json.JSONDecodeError, TypeError, ValueError) as e:
|
|
152
|
+
raise ModuleManifestError(f"{f} 无效: {e}") from e
|
|
153
|
+
if not cfg.name:
|
|
154
|
+
raise ModuleManifestError(f"{f} 缺少 'name'")
|
|
155
|
+
result.append(cfg)
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
def _load_commands(self, p: Path) -> list[CommandConfig]:
|
|
159
|
+
result: list[CommandConfig] = []
|
|
160
|
+
for f in sorted((p / "commands").glob("*.json")):
|
|
161
|
+
try:
|
|
162
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
163
|
+
cfg = CommandConfig.from_dict(data)
|
|
164
|
+
except (json.JSONDecodeError, TypeError, ValueError) as e:
|
|
165
|
+
raise ModuleManifestError(f"{f} 无效: {e}") from e
|
|
166
|
+
if not cfg.name:
|
|
167
|
+
raise ModuleManifestError(f"{f} 缺少 'name'")
|
|
168
|
+
result.append(cfg)
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
def _load_scripts(self, p: Path) -> dict[str, Any]:
|
|
172
|
+
"""加载 scripts/*.py 为可调用函数(exec 执行——加载目录视为可信代码)。"""
|
|
173
|
+
result: dict[str, Any] = {}
|
|
174
|
+
for f in sorted((p / "scripts").glob("*.py")):
|
|
175
|
+
ns: dict[str, Any] = {}
|
|
176
|
+
try:
|
|
177
|
+
exec(compile(f.read_text(encoding="utf-8"), str(f), "exec"), ns)
|
|
178
|
+
except Exception as e: # 脚本自身报错视为清单错误
|
|
179
|
+
raise ModuleManifestError(f"{f} 加载失败: {e}") from e
|
|
180
|
+
fn = ns.get(f.stem)
|
|
181
|
+
if not callable(fn):
|
|
182
|
+
raise ModuleManifestError(f"{f} 未定义函数 {f.stem}")
|
|
183
|
+
result[f.stem] = fn
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
def _load_guards(self, p: Path) -> dict[str, Any]:
|
|
187
|
+
"""加载 guards/*.py 为可调用函数(exec 执行——与 scripts 同机制)。"""
|
|
188
|
+
result: dict[str, Any] = {}
|
|
189
|
+
for f in sorted((p / "guards").glob("*.py")):
|
|
190
|
+
ns: dict[str, Any] = {}
|
|
191
|
+
try:
|
|
192
|
+
exec(compile(f.read_text(encoding="utf-8"), str(f), "exec"), ns)
|
|
193
|
+
except Exception as e: # 函数自身报错视为清单错误
|
|
194
|
+
raise ModuleManifestError(f"{f} 加载失败: {e}") from e
|
|
195
|
+
fn = ns.get(f.stem)
|
|
196
|
+
if not callable(fn):
|
|
197
|
+
raise ModuleManifestError(f"{f} 未定义函数 {f.stem}")
|
|
198
|
+
result[f.stem] = fn
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
def _load_submodules(self, p: Path) -> dict[str, SubModule]:
|
|
202
|
+
"""递归加载 submodules/*/(每个是完整子包)→ {目录名: 实例}。
|
|
203
|
+
|
|
204
|
+
目录名为引用键(pack 时以父模块 modules 的键命名),与子模块
|
|
205
|
+
自身 name 无关。guard 名不进入 provides/requires(边引用,不参与
|
|
206
|
+
重复名检测);子模块实例是父的 modules 值,加载时同样解析。"""
|
|
207
|
+
result: dict[str, SubModule] = {}
|
|
208
|
+
base = p / "submodules"
|
|
209
|
+
if not base.is_dir():
|
|
210
|
+
return result
|
|
211
|
+
for d in sorted(base.iterdir()):
|
|
212
|
+
if not (d / "module.json").is_file():
|
|
213
|
+
raise ModuleManifestError(f"{d} 缺少 module.json(submodule 目录无效)")
|
|
214
|
+
result[d.name] = self.load(d)
|
|
215
|
+
return result
|