修改agent流程,分步构建cdsl

This commit is contained in:
2026-08-27 18:59:33 +08:00
parent 7fc1dfa84d
commit b26db86d78
54 changed files with 6835 additions and 9984 deletions
+64 -371
View File
@@ -1,10 +1,10 @@
from __future__ import annotations
import copy
from functools import lru_cache
import json
import math
import re
import shutil
import sys
from pathlib import Path
from typing import Any
@@ -13,22 +13,9 @@ from jsonschema import Draft202012Validator
from jsonschema.exceptions import SchemaError
from vendor.cdsl_preview_runtime import step_to_glb
from app.services.quality import evaluate_quality, validate_verification
from app.services.storage import WorkspaceStore, now_iso, write_json
from app.services.cdsl_fragment import selector_bindings
from app.settings import Settings
class QualityVerificationError(RuntimeError):
"""A built CDSL document missed a blocking generic verification rule."""
def __init__(self, quality_report: dict[str, Any]) -> None:
super().__init__("Blocking CDSL verification checks failed")
self.quality_report = quality_report
self.task_id = ""
self.revision_id = ""
def load_engine(settings: Settings) -> Any:
parent = str(settings.engine_root.parent)
if parent not in sys.path:
@@ -50,8 +37,11 @@ def _walk(value: Any) -> list[tuple[str, Any]]:
return result
def _engine_schema(engine: Any) -> dict[str, Any]:
schema_path = Path(str(engine.__file__)).with_name("profile_schema.json")
@lru_cache(maxsize=16)
def _read_schema_document(path_value: str, modified_ns: int) -> dict[str, Any]:
"""Load an immutable runtime schema once per on-disk version."""
del modified_ns # The mtime is intentionally part of the cache key.
schema_path = Path(path_value)
try:
schema = json.loads(schema_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
@@ -61,12 +51,63 @@ def _engine_schema(engine: Any) -> dict[str, Any]:
return schema
def load_cdsl_json_schema(engine: Any) -> dict[str, Any]:
def _engine_schema(engine: Any) -> dict[str, Any]:
schema_path = Path(str(engine.__file__)).with_name("profile_schema.json")
try:
modified_ns = schema_path.stat().st_mtime_ns
except OSError as error:
raise RuntimeError("The local engine schema document is unavailable or invalid") from error
return _read_schema_document(str(schema_path), modified_ns)
def feature_atomic_contract(engine: Any, atomic_id: str) -> dict[str, Any]:
"""Return the runtime-supported parameter contract for one atomic feature."""
schema = _engine_schema(engine)
normalized_id = str(atomic_id or "").strip()
contracts = schema["feature_atomic_ids"]
contract = contracts.get(normalized_id)
registered = {str(item) for item in getattr(engine, "SUPPORTED_ATOMIC_IDS", ())}
declared = {str(item) for item in schema.get("runtime_supported_atomic_ids") or ()}
if normalized_id not in registered or normalized_id not in declared or not isinstance(contract, dict):
raise ValueError(f"Unsupported runtime atomic_id: {normalized_id}")
required = contract.get("required_params") or []
optional = contract.get("optional_params") or []
if not all(isinstance(item, str) and item for item in [*required, *optional]):
raise RuntimeError(f"Runtime feature contract is invalid for {normalized_id}")
return {
"atomic_id": normalized_id,
"summary": str(contract.get("summary") or ""),
"required_params": list(dict.fromkeys(required)),
"optional_params": [item for item in dict.fromkeys(optional) if item not in required],
"position_format": str(contract.get("position_format") or ""),
"requires_sketch": contract.get("requires_sketch") is True,
"selector_slot": copy.deepcopy(contract.get("selector_slot")) if isinstance(contract.get("selector_slot"), dict) else None,
}
@lru_cache(maxsize=16)
def _cdsl_validator(path_value: str, modified_ns: int) -> Draft202012Validator:
"""Compile the JSON Schema once per runtime schema revision."""
del modified_ns
schema_path = Path(path_value)
try:
schema = json.loads(schema_path.read_text(encoding="utf-8"))
Draft202012Validator.check_schema(schema)
except (OSError, json.JSONDecodeError, SchemaError) as error:
raise RuntimeError("The local CDSL JSON Schema is unavailable or invalid") from error
return Draft202012Validator(schema)
def _cdsl_schema_path(engine: Any) -> Path:
document = _engine_schema(engine)
schema_name = str(document.get("cdsl_json_schema_file") or "")
if not schema_name or Path(schema_name).name != schema_name:
raise RuntimeError("The local engine schema has an invalid CDSL JSON Schema path")
schema_path = Path(str(engine.__file__)).with_name(schema_name)
return Path(str(engine.__file__)).with_name(schema_name)
def load_cdsl_json_schema(engine: Any) -> dict[str, Any]:
schema_path = _cdsl_schema_path(engine)
try:
schema = json.loads(schema_path.read_text(encoding="utf-8"))
Draft202012Validator.check_schema(schema)
@@ -76,7 +117,11 @@ def load_cdsl_json_schema(engine: Any) -> dict[str, Any]:
def _validate_cdsl_json_schema(cdsl: dict[str, Any], engine: Any) -> None:
validator = Draft202012Validator(load_cdsl_json_schema(engine))
schema_path = _cdsl_schema_path(engine)
try:
validator = _cdsl_validator(str(schema_path), schema_path.stat().st_mtime_ns)
except OSError as error:
raise RuntimeError("The local CDSL JSON Schema is unavailable or invalid") from error
errors = sorted(validator.iter_errors(cdsl), key=lambda error: (list(error.absolute_path), error.message))
if not errors:
return
@@ -88,76 +133,6 @@ def _validate_cdsl_json_schema(cdsl: dict[str, Any], engine: Any) -> None:
raise ValueError(f"CDSL schema violation at {location}: {error.message}")
def _legacy_workplane(plane: str, offset: Any) -> dict[str, list[float]] | None:
if isinstance(offset, bool) or not isinstance(offset, (int, float)):
return None
distance = float(offset)
definitions = {
"XY": ([0.0, 0.0, distance], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]),
"XZ": ([0.0, distance, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
"YZ": ([distance, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]),
}
definition = definitions.get(plane.upper())
if definition is None:
return None
origin, x_dir, normal = definition
return {"origin_mm": origin, "x_dir": x_dir, "normal": normal}
def normalize_cdsl_for_engine(cdsl: dict[str, Any]) -> tuple[dict[str, Any], list[str]]:
"""Convert unambiguous legacy LLM aliases into the current CDSL dialect.
This intentionally does not infer dimensions, selectors, or feature
dependencies. Any non-mechanical error remains visible to the validator.
"""
normalized = copy.deepcopy(cdsl)
repairs: list[str] = []
geometry = normalized.get("geometry")
sketches = geometry.get("sketches") if isinstance(geometry, dict) else None
if isinstance(sketches, list):
for index, sketch in enumerate(sketches):
if not isinstance(sketch, dict):
continue
if "id" not in sketch and isinstance(sketch.get("sketch_id"), str):
sketch["id"] = sketch.pop("sketch_id")
repairs.append(f"geometry.sketches[{index}]: sketch_id -> id")
legacy_plane: Any = sketch.get("plane")
legacy_offset: Any = sketch.get("offset_mm", 0)
workplane_value = sketch.get("workplane")
if isinstance(workplane_value, dict) and "origin_mm" not in workplane_value:
legacy_plane = workplane_value.get("plane")
legacy_offset = workplane_value.get("offset_mm", 0)
elif "workplane" in sketch:
continue
workplane = _legacy_workplane(legacy_plane, legacy_offset) if isinstance(legacy_plane, str) else None
if workplane is None:
continue
sketch["workplane"] = workplane
sketch.pop("plane", None)
sketch.pop("offset_mm", None)
repairs.append(f"geometry.sketches[{index}]: legacy plane/offset_mm -> workplane")
features = normalized.get("features")
if isinstance(features, list):
for index, feature in enumerate(features):
if not isinstance(feature, dict):
continue
if "sketch_id" not in feature and isinstance(feature.get("sketch"), str):
feature["sketch_id"] = feature.pop("sketch")
repairs.append(f"features[{index}]: sketch -> sketch_id")
if "depends_on" not in feature:
feature["depends_on"] = []
repairs.append(f"features[{index}]: added empty depends_on")
params = feature.get("params")
axis = params.get("axis") if isinstance(params, dict) else None
if isinstance(axis, dict) and "origin_mm" not in axis and "point_mm" in axis:
axis["origin_mm"] = axis.pop("point_mm")
repairs.append(f"features[{index}].params.axis: point_mm -> origin_mm")
return normalized, repairs
def validate_cdsl(cdsl: dict[str, Any], engine: Any) -> None:
if not isinstance(cdsl, dict):
raise ValueError("CDSL must be a JSON object")
@@ -490,285 +465,3 @@ def topology_sidecars(
for name, point, normal, x_dir, y_dir in definitions
]
return ({"schema_version": "1.0", "references": references}, {"schema_version": "1.0", "edges": []})
def _set_parameter_value(document: dict[str, Any], path: list[str], value: float) -> None:
target: Any = document
for index, key in enumerate(path):
final = index == len(path) - 1
if isinstance(target, list):
item_index = int(key)
if item_index < 0 or item_index >= len(target):
raise ValueError("Parameter path is no longer valid")
if final:
target[item_index] = value
else:
target = target[item_index]
elif isinstance(target, dict):
if key not in target:
raise ValueError("Parameter path is no longer valid")
if final:
target[key] = value
else:
target = target[key]
else:
raise ValueError("Parameter path is no longer valid")
def apply_parameter_updates(cdsl: dict[str, Any], values: dict[str, float]) -> tuple[dict[str, Any], dict[str, Any]]:
contract = parameter_contract(cdsl)
entries = {str(item.get("id")): item for item in contract["parameters"]}
updated = copy.deepcopy(cdsl)
for parameter_id, raw_value in values.items():
entry = entries.get(parameter_id)
value = float(raw_value)
if entry is None or not entry.get("editable", False):
raise ValueError(f"Unknown editable parameter: {parameter_id}")
if not math.isfinite(value):
raise ValueError("Parameter values must be finite")
minimum, maximum = entry.get("minimum"), entry.get("maximum")
if isinstance(minimum, (int, float)) and value < float(minimum):
raise ValueError(f"{parameter_id} is below its minimum")
if isinstance(maximum, (int, float)) and value > float(maximum):
raise ValueError(f"{parameter_id} is above its maximum")
path = entry.get("path")
if not isinstance(path, list) or not all(isinstance(item, str) for item in path):
raise ValueError(f"{parameter_id} has an invalid path")
_set_parameter_value(updated, path, value)
declared = updated.get("meta", {}).get("editable_parameters")
if isinstance(declared, list):
for declared_entry in declared:
if isinstance(declared_entry, dict) and str(declared_entry.get("id")) == parameter_id:
declared_entry["value"] = value
return updated, parameter_contract(updated)
def _part_skill_audit(
part_skills: dict[str, Any] | None,
request: str,
generation_assumptions: list[str] | None,
) -> dict[str, Any]:
"""Normalize the planning audit persisted beside a product revision."""
audit = copy.deepcopy(part_skills) if isinstance(part_skills, dict) else {}
skills = [item for item in audit.get("skills") or [] if isinstance(item, dict)]
skill_ids = [str(item) for item in audit.get("skill_ids") or [] if str(item)]
if not skill_ids:
skill_ids = [str(item.get("id")) for item in skills if item.get("id")]
audit.update({
"schema_version": str(audit.get("schema_version") or "1.0"),
"request": str(audit.get("request") or request),
"structural_intent": str(audit.get("structural_intent") or request),
"skill_ids": skill_ids,
"skills": skills,
"inherited_skill_ids": [str(item) for item in audit.get("inherited_skill_ids") or [] if str(item)],
"assumptions": [str(item) for item in generation_assumptions or []],
})
return audit
def _generation_context(
reference_ids: list[str],
part_skill_audit: dict[str, Any],
) -> dict[str, Any]:
skills = [item for item in part_skill_audit.get("skills") or [] if isinstance(item, dict)]
return {
"cdsl_reference_ids": list(reference_ids),
"part_skill_ids": list(part_skill_audit.get("skill_ids") or []),
"part_skill_paths": [
{
"id": str(item.get("id") or ""),
"bridge": str(item.get("bridge") or ""),
"source": str(item.get("source") or ""),
}
for item in skills
],
"generation_assumptions": list(part_skill_audit.get("assumptions") or []),
}
def build_revision(
*,
settings: Settings,
store: WorkspaceStore,
task_id: str | None,
request: str,
cdsl: dict[str, Any],
reference_ids: list[str],
summary: str,
parent_revision_id: str | None = None,
operation: dict[str, Any] | None = None,
input_attachments: list[dict[str, Any]] | None = None,
part_skills: dict[str, Any] | None = None,
generation_assumptions: list[str] | None = None,
repair_attempts: int = 0,
verification: dict[str, Any] | None = None,
reference_records: list[dict[str, Any]] | None = None,
feature_plan: dict[str, Any] | None = None,
node_id: str = "",
fragment: dict[str, Any] | None = None,
branch_id: str = "main",
visibility: str = "final",
) -> dict[str, Any]:
engine = load_engine(settings)
task = store.ensure_task(task_id, request)
revision_id, revision_dir = store.next_revision(task["task_id"])
cdsl_path = revision_dir / "model.cdsl.json"
step_path = revision_dir / "model.step"
glb_path = revision_dir / "model.glb"
report_path = revision_dir / "rebuild-report.json"
request_path = revision_dir / "request.json"
references_path = revision_dir / "references.json"
parameters_path = revision_dir / "parameters.json"
selector_path = revision_dir / "model.selector.json"
edges_path = revision_dir / "model.edges.json"
topology_path = revision_dir / "model.topology.json"
part_skills_path = revision_dir / "part-skills.json"
quality_path = revision_dir / "quality-report.json"
snapshot_manifest_path = revision_dir / "snapshot-manifest.json"
fragment_path = revision_dir / "fragment.json"
selector_bindings_path = revision_dir / "selector-bindings.json"
part_skill_audit = _part_skill_audit(part_skills, request, generation_assumptions)
generation_context = _generation_context(reference_ids, part_skill_audit)
write_json(request_path, {"request": request, "created_at": now_iso()})
if isinstance(feature_plan, dict):
store.write_feature_plan(task["task_id"], feature_plan)
write_json(references_path, {"reference_ids": reference_ids, "records": [item for item in reference_records or [] if isinstance(item, dict)]})
write_json(part_skills_path, part_skill_audit)
write_json(snapshot_manifest_path, {
"schema_version": "1.0",
"status": "unavailable",
"reason": "Snapshot runner is not attached to this backend build",
"snapshots": [],
})
def revision_record(status: str, *, error: str = "", engine_name: str = "") -> dict[str, Any]:
record = {
"revision_id": revision_id,
"status": status,
"created_at": now_iso(),
"request_path": request_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"cdsl_path": cdsl_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"report_path": report_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"parameters_path": parameters_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"topology_path": topology_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"part_skills_path": part_skills_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"quality_path": quality_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"snapshot_manifest_path": snapshot_manifest_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"snapshot_status": "unavailable",
"snapshot_paths": [snapshot_manifest_path.relative_to(store.task_dir(task["task_id"])).as_posix()],
"part_skill_ids": list(part_skill_audit.get("skill_ids") or []),
"generation_assumptions": list(part_skill_audit.get("assumptions") or []),
"reference_ids": reference_ids,
"summary": summary,
"parent_revision_id": parent_revision_id or "",
"operation": operation or {},
"input_attachments": input_attachments or [],
"repair_attempts": max(0, int(repair_attempts)),
"branch_id": branch_id or "main",
"visibility": visibility if visibility in {"checkpoint", "final", "superseded"} else "checkpoint",
"node_id": node_id,
"fragment_path": fragment_path.relative_to(store.task_dir(task["task_id"])).as_posix() if fragment else "",
"selector_bindings_path": selector_bindings_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
}
if status == "success":
record.update({
"step_path": step_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"glb_path": glb_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"selector_path": selector_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"edges_path": edges_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"topology_path": topology_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
"engine": engine_name,
})
else:
record["error"] = error
return record
quality_report: dict[str, Any] | None = None
quality_status = ""
try:
cdsl_copy = copy.deepcopy(cdsl)
if not isinstance(cdsl_copy, dict):
raise ValueError("CDSL must be a JSON object")
cdsl_copy["part_id"] = task["task_id"]
meta = cdsl_copy.setdefault("meta", {})
if not isinstance(meta, dict):
raise ValueError("CDSL meta must be an object when present")
if not isinstance(meta.get("editable_parameters"), list) or not meta["editable_parameters"]:
meta["editable_parameters"] = _derived_parameters(cdsl_copy)
write_json(cdsl_path, cdsl_copy)
if fragment is not None:
write_json(fragment_path, fragment)
write_json(parameters_path, parameter_contract(cdsl_copy))
validate_cdsl(cdsl_copy, engine)
# Product revisions are semantic CDSL artifacts. Do not route them
# through the legacy rebuild entry point, which is allowed to use
# compiler_context/translator compatibility fallbacks.
engine_result = engine.run_cdsl_only(cdsl_copy, step_path)
if engine_result.get("engine") != "cdsl_only" or not step_path.is_file() or step_path.stat().st_size == 0:
raise RuntimeError("Engine did not produce a CDSL-only STEP artifact")
preview = step_to_glb(step_path, glb_path)
snapshot = topology_snapshot(engine_result, task_id=task["task_id"], revision_id=revision_id, preview=preview)
write_json(topology_path, snapshot)
write_json(selector_bindings_path, selector_bindings(
engine_result,
node_id=node_id,
snapshot_id=str(snapshot.get("snapshot_id") or ""),
))
selector, edges = topology_sidecars(engine_result, preview, snapshot=snapshot)
write_json(selector_path, selector)
write_json(edges_path, edges)
rules = validate_verification(verification, cdsl_copy)
quality_report = evaluate_quality(rules, cdsl_copy, engine_result)
quality_report["evaluated_at"] = now_iso()
write_json(quality_path, quality_report)
quality_status = (
"accepted" if rules and quality_report["status"] == "passed"
else "built_with_warnings" if quality_report["status"] == "passed"
else "needs_repair"
)
if quality_report["status"] != "passed":
raise QualityVerificationError(quality_report)
report = {
"engine_result": engine_result,
"preview": preview,
"generation_context": generation_context,
"validated_at": now_iso(),
}
write_json(report_path, report)
revision = revision_record("success", engine_name=str(engine_result["engine"]))
revision["quality_status"] = quality_status or "accepted"
revision["verification_summary"] = {
"requested": bool(rules),
"blocking_failures": len(quality_report.get("blocking_failures") or []),
"warnings": len(quality_report.get("warnings") or []),
}
except Exception as error:
if not isinstance(error, QualityVerificationError):
# The failed candidate is retained in the conversation diagnostics,
# not as a task revision. Runtime/schema failures must not create
# an editable revision that looks like a model version.
shutil.rmtree(revision_dir, ignore_errors=True)
raise
if not cdsl_path.is_file() and isinstance(cdsl, dict):
write_json(cdsl_path, copy.deepcopy(cdsl))
if quality_report is not None and not quality_path.is_file():
write_json(quality_path, quality_report)
write_json(report_path, {
"error": str(error),
"generation_context": generation_context,
"quality": quality_report,
"validated_at": now_iso(),
})
revision = revision_record("needs_repair" if quality_report is not None else "failed", error=str(error))
revision["quality_status"] = quality_status or ("needs_repair" if quality_report else "failed")
if quality_report is not None:
revision["verification_summary"] = {
"requested": bool(quality_report.get("verification_requested")),
"blocking_failures": len(quality_report.get("blocking_failures") or []),
"warnings": len(quality_report.get("warnings") or []),
}
store.update_task(task["task_id"], revision)
error.task_id = task["task_id"]
error.revision_id = revision_id
raise
store.update_task(task["task_id"], revision)
return {"task_id": task["task_id"], **revision}