d8d08facde
CI / build (pull_request) Successful in 22s
CI / lint (pull_request) Successful in 3m20s
CI / quality (pull_request) Successful in 3m55s
CI / security (pull_request) Successful in 4m2s
CI / typecheck (pull_request) Successful in 4m13s
CI / integration_tests (pull_request) Successful in 6m50s
CI / unit_tests (pull_request) Successful in 7m19s
CI / docker (pull_request) Successful in 1m8s
CI / e2e_tests (pull_request) Successful in 9m53s
CI / coverage (pull_request) Successful in 11m28s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Successful in 1h9m52s
Implements byte-size budget enforcement for the ACMS context assembly pipeline. enforce_size_budget() filters context fragments against max_file_size (per-fragment) and max_total_size (cumulative) limits defined in a ContextView. New domain models BudgetViolation and BudgetEnforcementResult provide structured violation reporting. Pipeline integration in ACMSPipeline.assemble() applies enforcement as a pre-filter when a context_view is provided. Review feedback addressed: - Fixed PR milestone to v3.4.0 (matching ticket #847) - Rebased branch onto latest master - Added CHANGELOG.md entry - Extracted duplicated enforcement logic into _apply_budget_enforcement() shared method on ACMSPipeline, called by both parent and subclass - Fixed _make_fragment return type from object to ContextFragment - Moved all imports to top of files (acms_pipeline.py, steps file) - Added errors="replace" to encode("utf-8") for surrogate safety - Documented thread-safety caveat on last_enforcement_result property - Added short-circuit early return when both limits are None - Added multi-byte Unicode content test scenario - Added total_size assertions to key scenarios - Added violation type verification to mixed limits scenario - Merged duplicate singular/plural step definitions - Added edge case scenarios (empty list, all exceed, exact boundary) - Fixed misleading docstring in ContextAssemblyPipeline.assemble() - Re-exported VALID_PHASES through core __init__.py public API ISSUES CLOSED: #847
370 lines
12 KiB
Python
370 lines
12 KiB
Python
"""Robot Framework helper for ACMS v1 pipeline integration tests.
|
|
|
|
Provides a CLI-style interface for Robot to invoke ACMS pipeline
|
|
operations and verify the results. Exit code 0 = success, 1 = failure.
|
|
|
|
Usage:
|
|
python robot/helper_acms_pipeline.py fragment-create
|
|
python robot/helper_acms_pipeline.py budget-calc
|
|
python robot/helper_acms_pipeline.py assemble-relevance
|
|
python robot/helper_acms_pipeline.py assemble-recency
|
|
python robot/helper_acms_pipeline.py assemble-tiered
|
|
python robot/helper_acms_pipeline.py payload-budget-check
|
|
python robot/helper_acms_pipeline.py assemble-size-budget
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from collections.abc import Callable
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
|
if _SRC in sys.path:
|
|
sys.path.remove(_SRC)
|
|
sys.path.insert(0, _SRC)
|
|
|
|
from cleveragents.application.services.acms_service import ACMSPipeline # noqa: E402
|
|
from cleveragents.domain.models.core.context_fragment import ( # noqa: E402
|
|
ContextBudget,
|
|
ContextFragment,
|
|
FragmentProvenance,
|
|
)
|
|
from cleveragents.domain.models.core.context_policy import ContextView # noqa: E402
|
|
|
|
# Default provenance for test fragments.
|
|
_DEFAULT_PROV = FragmentProvenance(resource_uri="test://robot")
|
|
|
|
|
|
def _cmd_fragment_create() -> int:
|
|
"""Create a ContextFragment and verify defaults."""
|
|
frag = ContextFragment(
|
|
uko_node="project://app/main.py",
|
|
content="hello world",
|
|
token_count=10,
|
|
provenance=FragmentProvenance(resource_uri="project://app/main.py"),
|
|
)
|
|
if frag.uko_node != "project://app/main.py":
|
|
print(
|
|
f"acms-fail: expected uko_node=project://app/main.py, got {frag.uko_node}"
|
|
)
|
|
return 1
|
|
if frag.relevance_score != 0.5:
|
|
print(f"acms-fail: expected score=0.5, got {frag.relevance_score}")
|
|
return 1
|
|
if not frag.fragment_id:
|
|
print("acms-fail: fragment_id is empty")
|
|
return 1
|
|
if not isinstance(frag.created_at, datetime):
|
|
print(f"acms-fail: created_at is not datetime, got {type(frag.created_at)}")
|
|
return 1
|
|
if frag.token_count != 10:
|
|
print(f"acms-fail: expected token_count=10, got {frag.token_count}")
|
|
return 1
|
|
print(f"acms-fragment-ok: id={frag.fragment_id}")
|
|
return 0
|
|
|
|
|
|
def _cmd_budget_calc() -> int:
|
|
"""Create ContextBudget and verify available_tokens."""
|
|
budget = ContextBudget(max_tokens=4096, reserved_tokens=512)
|
|
expected = 3584
|
|
if budget.available_tokens != expected:
|
|
print(
|
|
f"acms-fail: expected available={expected}, got {budget.available_tokens}"
|
|
)
|
|
return 1
|
|
print(f"acms-budget-ok: available={budget.available_tokens}")
|
|
return 0
|
|
|
|
|
|
def _cmd_assemble_relevance() -> int:
|
|
"""Assemble fragments with relevance strategy."""
|
|
frags = [
|
|
ContextFragment(
|
|
uko_node="project://app/high.py",
|
|
content="high",
|
|
relevance_score=0.9,
|
|
token_count=100,
|
|
provenance=_DEFAULT_PROV,
|
|
),
|
|
ContextFragment(
|
|
uko_node="project://app/low.py",
|
|
content="low",
|
|
relevance_score=0.2,
|
|
token_count=100,
|
|
provenance=_DEFAULT_PROV,
|
|
),
|
|
ContextFragment(
|
|
uko_node="project://app/mid.py",
|
|
content="mid",
|
|
relevance_score=0.6,
|
|
token_count=100,
|
|
provenance=_DEFAULT_PROV,
|
|
),
|
|
]
|
|
budget = ContextBudget(max_tokens=200, reserved_tokens=0)
|
|
pipeline = ACMSPipeline()
|
|
payload = pipeline.assemble(
|
|
plan_id="01JQTESTPN00000000000000AA",
|
|
fragments=frags,
|
|
budget=budget,
|
|
strategy="relevance",
|
|
)
|
|
if len(payload.fragments) != 2:
|
|
print(f"acms-fail: expected 2 fragments, got {len(payload.fragments)}")
|
|
return 1
|
|
if payload.fragments[0].content != "high":
|
|
print(f"acms-fail: expected first=high, got {payload.fragments[0].content}")
|
|
return 1
|
|
if not payload.strategies_used:
|
|
print("acms-fail: strategies_used is empty")
|
|
return 1
|
|
print(f"acms-assemble-relevance-ok: fragments={len(payload.fragments)}")
|
|
return 0
|
|
|
|
|
|
def _cmd_assemble_recency() -> int:
|
|
"""Assemble fragments with recency strategy."""
|
|
frags = [
|
|
ContextFragment(
|
|
uko_node="project://app/old.py",
|
|
content="old",
|
|
token_count=100,
|
|
created_at=datetime(2024, 1, 1, tzinfo=UTC),
|
|
provenance=_DEFAULT_PROV,
|
|
),
|
|
ContextFragment(
|
|
uko_node="project://app/mid.py",
|
|
content="mid",
|
|
token_count=100,
|
|
created_at=datetime(2024, 6, 1, tzinfo=UTC),
|
|
provenance=_DEFAULT_PROV,
|
|
),
|
|
ContextFragment(
|
|
uko_node="project://app/new.py",
|
|
content="new",
|
|
token_count=100,
|
|
created_at=datetime(2025, 1, 1, tzinfo=UTC),
|
|
provenance=_DEFAULT_PROV,
|
|
),
|
|
]
|
|
budget = ContextBudget(max_tokens=200, reserved_tokens=0)
|
|
pipeline = ACMSPipeline()
|
|
payload = pipeline.assemble(
|
|
plan_id="01JQTESTPN00000000000000AA",
|
|
fragments=frags,
|
|
budget=budget,
|
|
strategy="recency",
|
|
)
|
|
if len(payload.fragments) != 2:
|
|
print(f"acms-fail: expected 2 fragments, got {len(payload.fragments)}")
|
|
return 1
|
|
if payload.fragments[0].content != "new":
|
|
print(f"acms-fail: expected first=new, got {payload.fragments[0].content}")
|
|
return 1
|
|
if payload.fragments[1].content != "mid":
|
|
print(f"acms-fail: expected second=mid, got {payload.fragments[1].content}")
|
|
return 1
|
|
print(f"acms-assemble-recency-ok: fragments={len(payload.fragments)}")
|
|
return 0
|
|
|
|
|
|
def _cmd_assemble_tiered() -> int:
|
|
"""Assemble fragments with tiered strategy."""
|
|
frags = [
|
|
ContextFragment(
|
|
uko_node="project://app/cold.py",
|
|
content="cold-item",
|
|
relevance_score=0.9,
|
|
token_count=100,
|
|
tier="cold",
|
|
provenance=_DEFAULT_PROV,
|
|
),
|
|
ContextFragment(
|
|
uko_node="project://app/hot.py",
|
|
content="hot-item",
|
|
relevance_score=0.85,
|
|
token_count=100,
|
|
tier="hot",
|
|
provenance=_DEFAULT_PROV,
|
|
),
|
|
ContextFragment(
|
|
uko_node="project://app/warm.py",
|
|
content="warm-item",
|
|
relevance_score=0.8,
|
|
token_count=100,
|
|
tier="warm",
|
|
provenance=_DEFAULT_PROV,
|
|
),
|
|
]
|
|
budget = ContextBudget(max_tokens=200, reserved_tokens=0)
|
|
pipeline = ACMSPipeline()
|
|
payload = pipeline.assemble(
|
|
plan_id="01JQTESTPN00000000000000AA",
|
|
fragments=frags,
|
|
budget=budget,
|
|
strategy="tiered",
|
|
)
|
|
if len(payload.fragments) != 2:
|
|
print(f"acms-fail: expected 2 fragments, got {len(payload.fragments)}")
|
|
return 1
|
|
if payload.fragments[0].content != "hot-item":
|
|
print(f"acms-fail: expected first=hot-item, got {payload.fragments[0].content}")
|
|
return 1
|
|
print(f"acms-assemble-tiered-ok: fragments={len(payload.fragments)}")
|
|
return 0
|
|
|
|
|
|
def _cmd_payload_budget_check() -> int:
|
|
"""Verify is_within_budget property on assembled payload."""
|
|
frags = [
|
|
ContextFragment(
|
|
uko_node="project://app/item.py",
|
|
content="item",
|
|
relevance_score=0.9,
|
|
token_count=100,
|
|
provenance=_DEFAULT_PROV,
|
|
),
|
|
]
|
|
budget = ContextBudget(max_tokens=500, reserved_tokens=0)
|
|
pipeline = ACMSPipeline()
|
|
payload = pipeline.assemble(
|
|
plan_id="01JQTESTPN00000000000000AA",
|
|
fragments=frags,
|
|
budget=budget,
|
|
strategy="relevance",
|
|
)
|
|
if not payload.is_within_budget:
|
|
print("acms-fail: expected is_within_budget=True")
|
|
return 1
|
|
if payload.remaining_tokens != 400:
|
|
print(f"acms-fail: expected remaining=400, got {payload.remaining_tokens}")
|
|
return 1
|
|
if not payload.context_hash:
|
|
print("acms-fail: expected non-empty context_hash")
|
|
return 1
|
|
if len(payload.context_hash) != 64:
|
|
print(
|
|
f"acms-fail: expected SHA-256 hex (64 chars), "
|
|
f"got {len(payload.context_hash)}"
|
|
)
|
|
return 1
|
|
try:
|
|
int(payload.context_hash, 16)
|
|
except ValueError:
|
|
print(f"acms-fail: context_hash is not valid hex: {payload.context_hash!r}")
|
|
return 1
|
|
print(
|
|
f"acms-payload-budget-ok: within_budget={payload.is_within_budget} "
|
|
f"remaining={payload.remaining_tokens}"
|
|
)
|
|
return 0
|
|
|
|
|
|
def _cmd_assemble_size_budget() -> int:
|
|
"""Verify ACMSPipeline budget pre-filter with ContextView limits."""
|
|
frags = [
|
|
ContextFragment(
|
|
uko_node="project://app/small.py",
|
|
content="a" * 50,
|
|
relevance_score=0.9,
|
|
token_count=60,
|
|
provenance=_DEFAULT_PROV,
|
|
),
|
|
ContextFragment(
|
|
uko_node="project://app/too_big.py",
|
|
content="b" * 200,
|
|
relevance_score=0.8,
|
|
token_count=60,
|
|
provenance=_DEFAULT_PROV,
|
|
),
|
|
ContextFragment(
|
|
uko_node="project://app/fit.py",
|
|
content="c" * 80,
|
|
relevance_score=0.7,
|
|
token_count=60,
|
|
provenance=_DEFAULT_PROV,
|
|
),
|
|
]
|
|
budget = ContextBudget(max_tokens=1000, reserved_tokens=0)
|
|
pipeline = ACMSPipeline()
|
|
payload = pipeline.assemble(
|
|
plan_id="01JQTESTPN00000000000000AA",
|
|
fragments=frags,
|
|
budget=budget,
|
|
strategy="relevance",
|
|
context_view=ContextView(max_file_size=100),
|
|
)
|
|
|
|
result = pipeline.last_enforcement_result
|
|
if result is None:
|
|
print("acms-fail: expected non-empty enforcement result")
|
|
return 1
|
|
if len(payload.fragments) != 2:
|
|
print(f"acms-fail: expected 2 fragments, got {len(payload.fragments)}")
|
|
return 1
|
|
if len(result.accepted) != 2:
|
|
print(f"acms-fail: expected 2 accepted ids, got {len(result.accepted)}")
|
|
return 1
|
|
if len(result.violations) != 1:
|
|
print(f"acms-fail: expected 1 violation, got {len(result.violations)}")
|
|
return 1
|
|
if result.violations[0].violation_type != "max_file_size":
|
|
print(
|
|
"acms-fail: expected violation_type=max_file_size, "
|
|
f"got {result.violations[0].violation_type}"
|
|
)
|
|
return 1
|
|
|
|
payload_ids = {fragment.fragment_id for fragment in payload.fragments}
|
|
if payload_ids != set(result.accepted):
|
|
print(
|
|
"acms-fail: payload fragment ids do not match accepted ids "
|
|
f"payload={sorted(payload_ids)} accepted={sorted(result.accepted)}"
|
|
)
|
|
return 1
|
|
|
|
if result.total_size != 130:
|
|
print(f"acms-fail: expected total_size=130, got {result.total_size}")
|
|
return 1
|
|
|
|
print("acms-assemble-size-budget-ok: accepted=2 violations=1 total_size=130")
|
|
return 0
|
|
|
|
|
|
_COMMANDS: dict[str, Callable[[], int]] = {
|
|
"fragment-create": _cmd_fragment_create,
|
|
"budget-calc": _cmd_budget_calc,
|
|
"assemble-relevance": _cmd_assemble_relevance,
|
|
"assemble-recency": _cmd_assemble_recency,
|
|
"assemble-tiered": _cmd_assemble_tiered,
|
|
"payload-budget-check": _cmd_payload_budget_check,
|
|
"assemble-size-budget": _cmd_assemble_size_budget,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
"""Entry point called by Robot Framework ``Run Process``."""
|
|
if len(sys.argv) < 2:
|
|
print(
|
|
"Usage: helper_acms_pipeline.py "
|
|
"<fragment-create|budget-calc|assemble-relevance"
|
|
"|assemble-recency|assemble-tiered|payload-budget-check"
|
|
"|assemble-size-budget>"
|
|
)
|
|
return 1
|
|
|
|
command = sys.argv[1]
|
|
handler = _COMMANDS.get(command)
|
|
if handler is None:
|
|
print(f"Unknown command: {command}")
|
|
return 1
|
|
|
|
return handler()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|