Files
cleveragents-core/robot/helper_acms_pipeline_phase2.py
T
freemo fe7381c45b
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 14s
CI / build (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 27s
CI / security (pull_request) Successful in 32s
CI / typecheck (pull_request) Successful in 36s
CI / unit_tests (pull_request) Successful in 3m27s
CI / integration_tests (pull_request) Successful in 3m36s
CI / docker (pull_request) Successful in 42s
CI / coverage (pull_request) Successful in 4m50s
CI / lint (push) Successful in 13s
CI / build (push) Successful in 14s
CI / quality (push) Successful in 16s
CI / security (push) Successful in 32s
CI / typecheck (push) Successful in 36s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m1s
CI / docker (push) Successful in 51s
CI / integration_tests (push) Successful in 3m7s
CI / coverage (push) Successful in 4m23s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (pull_request) Successful in 29m6s
feat(acms): implement pipeline Phase 2 components
Add production-grade Phase 2 (Fragment Fusion) components for the ACMS
context assembly pipeline, replacing the no-op defaults:

- ContentHashDeduplicator: Groups fragments by UKO node URI, hashes
  content to detect duplicates, retains highest relevance_score.
- MaxDepthResolver: Resolves depth conflicts by keeping the highest
  detail depth per UKO node, with relevance tiebreaking.
- WeightedCompositeScorer: Computes composite score from configurable
  weighted factors (relevance=0.4, hierarchy=0.3, quality=0.2,
  recency=0.1). Stores component breakdown in metadata.
- GreedyKnapsackPacker: Greedy knapsack selection with depth fallback
  (tries depths [9,4,2,0] for oversized fragments) and minimum
  fragment token threshold (10).

Also adds:
- ScoredFragment frozen Pydantic model (spec §42825) with
  composite_score, score_components, and fragment reference
- score_detailed() method on WeightedCompositeScorer returning
  ScoredFragment objects for callers needing full breakdowns
- All components implement v1 Protocol signatures from acms_service.py
  and can be DI-injected into ACMSPipeline constructor

Testing:
- 31 Behave BDD scenarios in acms_pipeline_phase2.feature covering
  deduplication, depth resolution, scoring, packing, depth fallback,
  budget constraints, pipeline integration, and ScoredFragment model
- 6 Robot Framework integration smoke tests
- ASV benchmark suites for all 4 components and ScoredFragment

Quality gates: lint, typecheck (0 errors), unit_tests (8555 scenarios),
coverage (97.0%), dead_code — all passing.

ISSUES CLOSED: #540
2026-03-05 10:27:36 -05:00

182 lines
6.1 KiB
Python

"""Robot Framework helper for ACMS pipeline Phase 2 integration tests.
Provides a CLI-style interface for Robot to invoke Phase 2 pipeline
components and verify the results. Exit code 0 = success, 1 = failure.
Usage:
python robot/helper_acms_pipeline_phase2.py dedup-basic
python robot/helper_acms_pipeline_phase2.py depth-resolve
python robot/helper_acms_pipeline_phase2.py scorer-default
python robot/helper_acms_pipeline_phase2.py packer-budget
python robot/helper_acms_pipeline_phase2.py scored-fragment
python robot/helper_acms_pipeline_phase2.py pipeline-integration
"""
from __future__ import annotations
import sys
from collections.abc import Callable
from pathlib import Path
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.application.services.acms_phase2 import ( # noqa: E402
ContentHashDeduplicator,
GreedyKnapsackPacker,
MaxDepthResolver,
WeightedCompositeScorer,
)
from cleveragents.application.services.acms_service import ACMSPipeline # noqa: E402
from cleveragents.domain.models.core.context_fragment import ( # noqa: E402
ContextBudget,
ContextFragment,
FragmentProvenance,
ScoredFragment,
)
_DEFAULT_PROV = FragmentProvenance(resource_uri="test://robot-phase2")
_PLAN_ID = "01JQTESTPN00000000000000CC"
def _frag(
uko_node: str = "test://default",
content: str = "test content",
score: float = 0.5,
tokens: int = 10,
depth: int = 0,
) -> ContextFragment:
return ContextFragment(
uko_node=uko_node,
content=content,
relevance_score=score,
token_count=tokens,
detail_depth=depth,
provenance=_DEFAULT_PROV,
)
def _cmd_dedup_basic() -> int:
"""Deduplicate identical fragments and keep highest scored."""
dedup = ContentHashDeduplicator()
fragments = [
_frag(uko_node="project://a.py", content="hello", score=0.5),
_frag(uko_node="project://a.py", content="hello", score=0.9),
_frag(uko_node="project://b.py", content="hello", score=0.7),
]
result = dedup.deduplicate(fragments)
assert len(result) == 2, f"Expected 2, got {len(result)}"
# Find the fragment for a.py — should be the higher-scored one
a_frags = [f for f in result if f.uko_node == "project://a.py"]
assert len(a_frags) == 1
assert a_frags[0].relevance_score == 0.9
print("phase2-dedup-ok")
return 0
def _cmd_depth_resolve() -> int:
"""Resolve depth conflicts to max depth per node."""
resolver = MaxDepthResolver()
fragments = [
_frag(uko_node="project://a.py", content="brief", depth=2, tokens=10),
_frag(uko_node="project://a.py", content="full", depth=9, tokens=100),
_frag(uko_node="project://b.py", content="b", depth=5, tokens=50),
]
result = resolver.resolve(fragments)
assert len(result) == 2, f"Expected 2, got {len(result)}"
a_frags = [f for f in result if f.uko_node == "project://a.py"]
assert a_frags[0].detail_depth == 9
print("phase2-depth-ok")
return 0
def _cmd_scorer_default() -> int:
"""Score fragments with default weights."""
scorer = WeightedCompositeScorer()
fragments = [
_frag(uko_node="project://a.py", content="hello", score=0.8),
_frag(uko_node="project://b.py", content="world", score=0.4),
]
result = scorer.score(fragments)
assert len(result) == 2
for f in result:
assert 0.0 <= f.relevance_score <= 1.0
assert "_original_relevance" in f.metadata
print("phase2-scorer-ok")
return 0
def _cmd_packer_budget() -> int:
"""Pack fragments into budget."""
packer = GreedyKnapsackPacker()
fragments = [
_frag(uko_node="project://a.py", content="alpha", score=0.9, tokens=100),
_frag(uko_node="project://b.py", content="beta", score=0.7, tokens=100),
_frag(uko_node="project://c.py", content="gamma", score=0.5, tokens=100),
]
budget = ContextBudget(max_tokens=250, reserved_tokens=0)
result = packer.pack(fragments, budget)
assert len(result) == 2, f"Expected 2, got {len(result)}"
total = sum(f.token_count for f in result)
assert total <= 250, f"Total {total} exceeds budget 250"
print("phase2-packer-ok")
return 0
def _cmd_scored_fragment() -> int:
"""Create and verify ScoredFragment model."""
frag = _frag(uko_node="project://a.py", content="test", score=0.8)
scored = ScoredFragment(
fragment=frag,
composite_score=0.85,
score_components={"relevance": 0.9, "hierarchy": 0.8},
)
assert scored.composite_score == 0.85
assert scored.score_components["relevance"] == 0.9
assert scored.fragment is frag
print("phase2-scored-fragment-ok")
return 0
def _cmd_pipeline_integration() -> int:
"""Inject Phase 2 components into ACMSPipeline."""
pipeline = ACMSPipeline(
deduplicator=ContentHashDeduplicator(),
depth_resolver=MaxDepthResolver(),
scorer=WeightedCompositeScorer(),
packer=GreedyKnapsackPacker(),
)
fragments = [
_frag(uko_node="project://a.py", content="hello", score=0.9, tokens=50),
_frag(uko_node="project://a.py", content="hello", score=0.5, tokens=50),
_frag(uko_node="project://b.py", content="world", score=0.7, tokens=50),
]
budget = ContextBudget(max_tokens=200, reserved_tokens=0)
payload = pipeline.assemble(
plan_id=_PLAN_ID,
fragments=fragments,
budget=budget,
)
assert len(payload.fragments) >= 1
assert payload.total_tokens <= 200
print("phase2-pipeline-ok")
return 0
_COMMANDS: dict[str, Callable[[], int]] = {
"dedup-basic": _cmd_dedup_basic,
"depth-resolve": _cmd_depth_resolve,
"scorer-default": _cmd_scorer_default,
"packer-budget": _cmd_packer_budget,
"scored-fragment": _cmd_scored_fragment,
"pipeline-integration": _cmd_pipeline_integration,
}
if __name__ == "__main__":
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr)
sys.exit(1)
sys.exit(_COMMANDS[sys.argv[1]]())