添加cadfs_to_cdsl代码,优化cad生成
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
"""Create a resumable live-evaluation task without starting its workflow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from dataclasses import replace
|
||||
import json
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import sys
|
||||
|
||||
from app.cad_agent.composition import compose_v3
|
||||
from app.settings import get_settings
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Create one isolated live CAD task for resume_one_step.")
|
||||
parser.add_argument("--report-root", required=True, type=Path)
|
||||
parser.add_argument("--prompt", required=True)
|
||||
parser.add_argument("--task-id")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
arguments = _arguments()
|
||||
root = arguments.report_root.resolve()
|
||||
settings = get_settings()
|
||||
services = compose_v3(replace(
|
||||
settings,
|
||||
task_root=root / "artifacts",
|
||||
conversation_root=root / "conversations",
|
||||
))
|
||||
task_id = arguments.task_id or f"cad_{secrets.token_hex(6)}"
|
||||
services.workflow.create_task(task_id, arguments.prompt)
|
||||
print(json.dumps({"task_id": task_id, "report_root": str(root)}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -36,7 +36,7 @@ from app.cad_agent.evals.token_baseline import (
|
||||
from app.settings import BACKEND_ROOT, get_settings
|
||||
|
||||
|
||||
_CANDIDATE_EVIDENCE_FILES = frozenset({
|
||||
_LEGACY_CANDIDATE_EVIDENCE_FILES = frozenset({
|
||||
"candidate.json",
|
||||
"candidate-review.json",
|
||||
"model.cdsl.json",
|
||||
@@ -48,6 +48,19 @@ _CANDIDATE_EVIDENCE_FILES = frozenset({
|
||||
"renders/contact-sheet.jpg",
|
||||
})
|
||||
|
||||
# A v3.2 Feature DAG publishes one atomic checkpoint after local verification.
|
||||
# It deliberately has no candidate review or technical render bundle; the
|
||||
# final review owns that later evidence. GLB is optional because a preview
|
||||
# conversion outage must not invalidate an otherwise sound STEP checkpoint.
|
||||
_FEATURE_NODE_EVIDENCE_FILES = frozenset({
|
||||
"input.json",
|
||||
"model.cdsl.json",
|
||||
"model.step",
|
||||
"model.topology.json",
|
||||
"node-verification.json",
|
||||
"rebuild-report.json",
|
||||
})
|
||||
|
||||
_FAILURE_LAYERS = frozenset({
|
||||
"model_format_or_decision",
|
||||
"v3_contract_or_verifier",
|
||||
@@ -240,13 +253,14 @@ def _artifact_evidence_complete(
|
||||
revisions: list[str],
|
||||
active_revision: str,
|
||||
artifact_manifest: dict[str, Any] | None,
|
||||
ledger: list[dict[str, Any]],
|
||||
) -> bool:
|
||||
"""Require every published CAD decision to retain its reviewable evidence.
|
||||
|
||||
A non-empty report manifest is not enough: a task could otherwise report
|
||||
only a rendered contract view while silently losing the STEP, topology, or
|
||||
candidate review used to accept a revision. Candidate manifests protect
|
||||
the immutable build inputs/outputs; the report manifest additionally
|
||||
node verification used to accept a revision. Checkpoint manifests protect
|
||||
immutable build inputs and outputs; the report manifest additionally
|
||||
protects the frozen contract and final independent review written later.
|
||||
"""
|
||||
if not revisions or not active_revision:
|
||||
@@ -264,9 +278,19 @@ def _artifact_evidence_complete(
|
||||
return False
|
||||
if not all((artifact_root / path).is_file() for path in required_report_files):
|
||||
return False
|
||||
feature_revisions = {
|
||||
str(item.get("revision_id") or "")
|
||||
for item in ledger
|
||||
if isinstance(item, dict) and item.get("event") == "feature_node_verified"
|
||||
}
|
||||
for revision_id in sorted(set(revisions)):
|
||||
revision_root = artifact_root / "revisions" / revision_id
|
||||
required_paths = {revision_root / relative for relative in _CANDIDATE_EVIDENCE_FILES}
|
||||
evidence_files = (
|
||||
_FEATURE_NODE_EVIDENCE_FILES
|
||||
if revision_id in feature_revisions
|
||||
else _LEGACY_CANDIDATE_EVIDENCE_FILES
|
||||
)
|
||||
required_paths = {revision_root / relative for relative in evidence_files}
|
||||
manifest_path = revision_root / "manifest.json"
|
||||
if not manifest_path.is_file() or not all(path.is_file() for path in required_paths):
|
||||
return False
|
||||
@@ -275,7 +299,7 @@ def _artifact_evidence_complete(
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return False
|
||||
declared = manifest.get("files") if isinstance(manifest, dict) else None
|
||||
if not isinstance(declared, dict) or not _CANDIDATE_EVIDENCE_FILES.issubset(declared):
|
||||
if not isinstance(declared, dict) or not evidence_files.issubset(declared):
|
||||
return False
|
||||
for relative, digest in declared.items():
|
||||
path = revision_root / str(relative)
|
||||
@@ -284,7 +308,7 @@ def _artifact_evidence_complete(
|
||||
if sha256(path.read_bytes()).hexdigest() != digest:
|
||||
return False
|
||||
if artifact_manifest is not None:
|
||||
expected_report_paths = {f"revisions/{revision_id}/{relative}" for relative in _CANDIDATE_EVIDENCE_FILES}
|
||||
expected_report_paths = {f"revisions/{revision_id}/{relative}" for relative in evidence_files}
|
||||
if not expected_report_paths.issubset(report_files):
|
||||
return False
|
||||
return True
|
||||
@@ -356,9 +380,24 @@ def _run_checks(
|
||||
reviewer_calls = [item for item in records if isinstance(item, dict) and item.get("role") == "reviewer"]
|
||||
total_tokens = int(usage.get("prompt_tokens") or 0) + int(usage.get("completion_tokens") or 0)
|
||||
audit_ledger = ledger if ledger is not None else [item for item in projection.get("action_ledger_summary") or () if isinstance(item, dict)]
|
||||
accepted = [item for item in audit_ledger if isinstance(item, dict) and item.get("event") == "accepted"]
|
||||
revisions = [str(item.get("revision_id") or "") for item in accepted]
|
||||
operation_ids = {str(item.get("actual_atomic_id") or "") for item in accepted}
|
||||
published = [
|
||||
item for item in audit_ledger
|
||||
if isinstance(item, dict) and item.get("event") in {"accepted", "feature_node_verified"}
|
||||
]
|
||||
revisions = [str(item.get("revision_id") or "") for item in published]
|
||||
scheduled_atomic_ids = {
|
||||
str(item.get("node_id") or ""): str(item.get("atomic_id") or "")
|
||||
for item in audit_ledger
|
||||
if isinstance(item, dict) and item.get("event") == "feature_node_scheduled"
|
||||
}
|
||||
operation_ids = {
|
||||
str(
|
||||
item.get("actual_atomic_id")
|
||||
or item.get("atomic_id")
|
||||
or scheduled_atomic_ids.get(str(item.get("node_id") or ""), "")
|
||||
)
|
||||
for item in published
|
||||
}
|
||||
required_operation_groups = [
|
||||
{str(atomic_id) for atomic_id in group if isinstance(atomic_id, str) and atomic_id}
|
||||
for group in scenario.get("required_any_atomic_id_groups") or ()
|
||||
@@ -386,6 +425,7 @@ def _run_checks(
|
||||
revisions,
|
||||
active_revision,
|
||||
artifact_manifest,
|
||||
audit_ledger,
|
||||
)
|
||||
return {
|
||||
"expected_terminal_phase": projection.get("phase") == scenario.get("expected_phase", "COMPLETED"),
|
||||
@@ -712,7 +752,7 @@ async def _run(arguments: argparse.Namespace, report_root: Path) -> dict[str, An
|
||||
"reviewer": str(reviewer_capability.get("mode") or ""),
|
||||
},
|
||||
"git_revision": _git_revision(),
|
||||
"protocol_version": "3.0",
|
||||
"protocol_version": "3.2",
|
||||
"runtime_profile_sha256": runtime_profile_hash,
|
||||
"operation_contracts": contracts,
|
||||
"verifier_registry_version": "cad.verifier-registry.v1",
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Advance one persisted event for a task in an isolated live-evaluation root.
|
||||
|
||||
Long real-provider evaluations can outlive a command host's execution window.
|
||||
This utility takes exactly one event from ``WorkflowCoordinator.run`` and
|
||||
closes the async generator after that event has been durably handled. Repeated
|
||||
invocations therefore resume the same task without re-running already
|
||||
persisted nodes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
from dataclasses import replace
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from app.cad_agent.application.workflow import ModelIdentity
|
||||
from app.cad_agent.composition import compose_v3
|
||||
from app.settings import get_settings
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Advance one event for an isolated live CAD evaluation task.")
|
||||
parser.add_argument("--report-root", required=True, type=Path)
|
||||
parser.add_argument("--task-id", required=True)
|
||||
parser.add_argument("--author-provider")
|
||||
parser.add_argument("--author-model")
|
||||
parser.add_argument("--review-provider")
|
||||
parser.add_argument("--review-model")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
async def _advance(arguments: argparse.Namespace) -> dict[str, Any]:
|
||||
settings = get_settings()
|
||||
author_provider, author_model = settings.resolve_model(arguments.author_provider, arguments.author_model)
|
||||
if arguments.review_provider or arguments.review_model:
|
||||
review_provider, review_model = settings.resolve_model(arguments.review_provider, arguments.review_model)
|
||||
else:
|
||||
review_provider, review_model = settings.resolve_independent_review_model(author_provider, author_model)
|
||||
report_root = arguments.report_root.resolve()
|
||||
services = compose_v3(replace(
|
||||
settings,
|
||||
task_root=report_root / "artifacts",
|
||||
conversation_root=report_root / "conversations",
|
||||
))
|
||||
before = services.repository.get_state(arguments.task_id)
|
||||
if before is None:
|
||||
raise ValueError(f"Unknown task {arguments.task_id!r} in {report_root}")
|
||||
runner = services.workflow.run(
|
||||
task_id=arguments.task_id,
|
||||
author=ModelIdentity(author_provider.id, author_model.id),
|
||||
reviewer=ModelIdentity(review_provider.id, review_model.id),
|
||||
)
|
||||
try:
|
||||
name, payload = await anext(runner)
|
||||
except StopAsyncIteration:
|
||||
name, payload = "workflow_exhausted", {}
|
||||
finally:
|
||||
await runner.aclose()
|
||||
after = services.repository.get_state(arguments.task_id)
|
||||
return {
|
||||
"task_id": arguments.task_id,
|
||||
"event": {"name": name, "payload": payload},
|
||||
"before": {"phase": before.phase.value, "version": before.version},
|
||||
"after": {
|
||||
"phase": after.phase.value if after is not None else "",
|
||||
"version": after.version if after is not None else -1,
|
||||
"active_revision": after.active_revision if after is not None else "",
|
||||
"last_error": after.last_error.value if after is not None and after.last_error else "",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
arguments = _arguments()
|
||||
try:
|
||||
result = asyncio.run(_advance(arguments))
|
||||
except BaseException as error:
|
||||
result = {"error": f"{type(error).__name__}: {str(error)[:1000]}"}
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return 2
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user