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
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
182 lines
6.1 KiB
Python
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]]())
|