"""Step definitions for features/acms_pipeline_phase3.feature. Tests the ACMS pipeline Phase 3 (Context Finalization) components and advanced context strategies directly in-memory — no database required. """ from __future__ import annotations from typing import Any from behave import given, then, when from behave.runner import Context from cleveragents.application.services.acms_advanced_strategies import ( ArceStrategy, PlanDecisionContextStrategy, TemporalArchaeologyStrategy, ) from cleveragents.application.services.acms_phase3 import ( ProvenancePreambleGenerator, RelevanceCoherenceOrderer, ) from cleveragents.application.services.acms_service import ACMSPipeline from cleveragents.application.services.acms_skeleton_compressor import ( DepthReductionCompressor, ) from cleveragents.domain.models.core.context_fragment import ( ContextBudget, ContextFragment, FragmentProvenance, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _DEFAULT_PROVENANCE = FragmentProvenance(resource_uri="test://phase3") _TEST_PLAN_ID = "01JQTESTPN00000000000000DD" def _make_phase3_fragment(**kwargs: Any) -> ContextFragment: """Create a ContextFragment with sensible Phase 3 test defaults.""" kwargs.setdefault("uko_node", "test://default") kwargs.setdefault("token_count", 0) kwargs.setdefault("provenance", _DEFAULT_PROVENANCE) return ContextFragment(**kwargs) # --------------------------------------------------------------------------- # Fragment list construction — Given steps # --------------------------------------------------------------------------- @given("the following phase3 fragments:") def step_given_phase3_fragments(context: Context) -> None: frags: list[ContextFragment] = [] for row in context.table: frags.append( _make_phase3_fragment( uko_node=row["uko_node"], content=row["content"].replace("\\n", "\n"), relevance_score=float(row["score"]), token_count=int(row["tokens"]), detail_depth=int(row["depth"]), ) ) context.phase3_fragments = frags @given("the following phase3 fragments with strategy sources:") def step_given_phase3_fragments_with_strategy(context: Context) -> None: frags: list[ContextFragment] = [] for row in context.table: frags.append( _make_phase3_fragment( uko_node=row["uko_node"], content=row["content"].replace("\\n", "\n"), relevance_score=float(row["score"]), token_count=int(row["tokens"]), detail_depth=int(row["depth"]), strategy_source=row["strategy_source"], ) ) context.phase3_fragments = frags @given("the following phase3 skeleton fragments:") def step_given_phase3_skeleton_fragments(context: Context) -> None: frags: list[ContextFragment] = [] for row in context.table: metadata = {} if row["domain"].strip(): metadata["detail_level_domain"] = row["domain"] frags.append( _make_phase3_fragment( uko_node=row["uko_node"], content=row["content"].replace("\\n", "\n"), relevance_score=float(row["score"]), token_count=int(row["tokens"]), detail_depth=int(row["depth"]), provenance=FragmentProvenance( resource_uri=row["uko_node"], resource_type=row["resource_type"], ), metadata=metadata, ) ) context.phase3_skeleton_fragments = tuple(frags) @given("an empty phase3 fragment list") def step_given_empty_phase3(context: Context) -> None: context.phase3_fragments = [] # --------------------------------------------------------------------------- # Strategy fragment construction — Given steps # --------------------------------------------------------------------------- @given("the following strategy3 fragments:") def step_given_strategy3_fragments(context: Context) -> None: frags: list[ContextFragment] = [] for row in context.table: frags.append( _make_phase3_fragment( uko_node=row["uko_node"], content=row["content"].replace("\\n", "\n"), relevance_score=float(row["score"]), token_count=int(row["tokens"]), detail_depth=int(row["depth"]), ) ) context.strategy3_fragments = frags @given("the following strategy3 fragments with tiers:") def step_given_strategy3_fragments_with_tiers(context: Context) -> None: frags: list[ContextFragment] = [] for row in context.table: frags.append( _make_phase3_fragment( uko_node=row["uko_node"], content=row["content"].replace("\\n", "\n"), relevance_score=float(row["score"]), token_count=int(row["tokens"]), detail_depth=int(row["depth"]), tier=row["tier"], ) ) context.strategy3_fragments = frags @given("an empty strategy3 fragment list") def step_given_empty_strategy3(context: Context) -> None: context.strategy3_fragments = [] @given("a strategy3 budget with max_tokens {max_t:d} and reserved_tokens {res_t:d}") def step_given_strategy3_budget(context: Context, max_t: int, res_t: int) -> None: context.strategy3_budget = ContextBudget(max_tokens=max_t, reserved_tokens=res_t) # --------------------------------------------------------------------------- # Strategy constructors — Given steps # --------------------------------------------------------------------------- @given("an ArceStrategy with max_iterations {max_iter:d}") def step_given_arce_strategy(context: Context, max_iter: int) -> None: context.arce_strategy = ArceStrategy(max_iterations=max_iter) @given("a TemporalArchaeologyStrategy") def step_given_temporal_strategy(context: Context) -> None: context.temporal_strategy = TemporalArchaeologyStrategy() @given("a PlanDecisionContextStrategy") def step_given_plan_decision_strategy(context: Context) -> None: context.plan_decision_strategy = PlanDecisionContextStrategy() # --------------------------------------------------------------------------- # RelevanceCoherenceOrderer — When / Then # --------------------------------------------------------------------------- @when("I order the fragments with RelevanceCoherenceOrderer") def step_order_fragments(context: Context) -> None: orderer = RelevanceCoherenceOrderer() context.phase3_result = list(orderer.order(context.phase3_fragments)) @then('the first ordered fragment should have uko_node "{expected}"') def step_first_ordered_node(context: Context, expected: str) -> None: assert len(context.phase3_result) >= 1 assert context.phase3_result[0].uko_node == expected, ( f"Expected {expected}, got {context.phase3_result[0].uko_node}" ) @then("fragments from the same group should be adjacent") def step_same_group_adjacent(context: Context) -> None: # Verify that fragments with the same prefix are adjacent seen_prefixes: set[str] = set() current_prefix: str | None = None for frag in context.phase3_result: prefix = _extract_prefix(frag.uko_node) if prefix != current_prefix: assert prefix not in seen_prefixes, ( f"Prefix {prefix} appeared non-contiguously" ) if current_prefix is not None: seen_prefixes.add(current_prefix) current_prefix = prefix @then("{count:d} fragments should remain after ordering") def step_order_count_plural(context: Context, count: int) -> None: assert len(context.phase3_result) == count, ( f"Expected {count}, got {len(context.phase3_result)}" ) @then("{count:d} fragment should remain after ordering") def step_order_count_singular(context: Context, count: int) -> None: assert len(context.phase3_result) == count, ( f"Expected {count}, got {len(context.phase3_result)}" ) # --------------------------------------------------------------------------- # ProvenancePreambleGenerator — When / Then # --------------------------------------------------------------------------- @when("I generate a preamble with ProvenancePreambleGenerator") def step_generate_preamble(context: Context) -> None: generator = ProvenancePreambleGenerator() context.phase3_preamble = generator.generate(context.phase3_fragments) @then('the preamble should contain "{text}"') def step_preamble_contains(context: Context, text: str) -> None: assert context.phase3_preamble is not None, "Preamble was None" assert text in context.phase3_preamble, ( f"Expected preamble to contain '{text}', got:\n{context.phase3_preamble}" ) @then("the preamble should be None") def step_preamble_is_none(context: Context) -> None: assert context.phase3_preamble is None # --------------------------------------------------------------------------- # DepthReductionCompressor — When / Then # --------------------------------------------------------------------------- @when("I compress with DepthReductionCompressor and budget {budget:d}") def step_compress_depth_reduction(context: Context, budget: int) -> None: compressor = DepthReductionCompressor() source = getattr(context, "phase3_skeleton_fragments", ()) context.phase3_compressed = compressor.compress(source, budget) @then("all compressed fragments should have detail depth at most {max_depth:d}") def step_compressed_depth_max(context: Context, max_depth: int) -> None: assert all(f.detail_depth <= max_depth for f in context.phase3_compressed), ( f"Expected all compressed fragments to have depth <= {max_depth}, " f"got {[f.detail_depth for f in context.phase3_compressed]}" ) @then("compressed fragments should fit within budget {budget:d}") def step_compressed_budget(context: Context, budget: int) -> None: used = sum(fragment.token_count for fragment in context.phase3_compressed) assert used <= budget, ( f"Compressed fragments used {used} tokens for budget {budget}" ) @then('the compressed fragments should include skeleton level "{level}"') def step_compressed_level(context: Context, level: str) -> None: levels = { fragment.metadata.get("skeleton_target_level") for fragment in context.phase3_compressed } assert level in levels, ( f"Expected level {level!r}, got {sorted(level for level in levels if level is not None)}" ) @then("{count:d} fragments should remain after compression") def step_compressed_count(context: Context, count: int) -> None: assert len(context.phase3_compressed) == count, ( f"Expected {count} compressed fragments, got {len(context.phase3_compressed)}" ) @then('a compressed fragment should contain "{snippet}"') def step_compressed_content_contains(context: Context, snippet: str) -> None: contents = [fragment.content for fragment in context.phase3_compressed] assert any(snippet in content for content in contents), ( f"Expected a compressed fragment to contain {snippet!r}, got {contents!r}" ) # --------------------------------------------------------------------------- # ArceStrategy — When / Then # --------------------------------------------------------------------------- @when("I assemble with the ArceStrategy") def step_assemble_arce(context: Context) -> None: context.arce_result = list( context.arce_strategy.assemble( context.strategy3_fragments, context.strategy3_budget, ) ) @when("I check can_handle on ArceStrategy") def step_check_arce_can_handle(context: Context) -> None: context.arce_confidence = context.arce_strategy.can_handle({}) @then("fragments should be returned by arce strategy") def step_arce_has_results(context: Context) -> None: assert len(context.arce_result) > 0 @then("the arce result should respect the budget") def step_arce_respects_budget(context: Context) -> None: total = sum(f.token_count for f in context.arce_result) available = context.strategy3_budget.available_tokens assert total <= available, f"ARCE used {total} tokens, budget was {available}" @then("{count:d} fragments should be returned by arce strategy") def step_arce_exact_count(context: Context, count: int) -> None: assert len(context.arce_result) == count @then("at most {count:d} fragments should be returned by arce strategy") def step_arce_at_most_count(context: Context, count: int) -> None: assert len(context.arce_result) <= count, ( f"Expected at most {count}, got {len(context.arce_result)}" ) @then("the arce confidence should be {expected:g}") def step_arce_confidence(context: Context, expected: float) -> None: assert context.arce_confidence == expected @then('the ArceStrategy name should be "{expected}"') def step_arce_name(context: Context, expected: str) -> None: assert context.arce_strategy.name == expected @then("the ArceStrategy should support semantic search") def step_arce_supports_semantic(context: Context) -> None: assert context.arce_strategy.capabilities.supports_semantic_search @then('the ArceStrategy explain should contain "{text}"') def step_arce_explain(context: Context, text: str) -> None: explanation = context.arce_strategy.explain() assert text in explanation, ( f"Expected explain to contain '{text}', got: {explanation}" ) # --------------------------------------------------------------------------- # TemporalArchaeologyStrategy — When / Then # --------------------------------------------------------------------------- @when("I assemble with the TemporalArchaeologyStrategy") def step_assemble_temporal(context: Context) -> None: context.temporal_result = list( context.temporal_strategy.assemble( context.strategy3_fragments, context.strategy3_budget, ) ) @when("I check can_handle on TemporalArchaeologyStrategy") def step_check_temporal_can_handle(context: Context) -> None: context.temporal_confidence = context.temporal_strategy.can_handle({}) @then('the first temporal result fragment should have uko_node "{expected}"') def step_temporal_first_node(context: Context, expected: str) -> None: assert len(context.temporal_result) >= 1 assert context.temporal_result[0].uko_node == expected, ( f"Expected {expected}, got {context.temporal_result[0].uko_node}" ) @then("{count:d} fragments should be returned by temporal strategy") def step_temporal_exact_count(context: Context, count: int) -> None: assert len(context.temporal_result) == count @then("at most {count:d} fragments should be returned by temporal strategy") def step_temporal_at_most_count(context: Context, count: int) -> None: assert len(context.temporal_result) <= count @then("the temporal confidence should be {expected:g}") def step_temporal_confidence(context: Context, expected: float) -> None: assert context.temporal_confidence == expected @then('the TemporalArchaeologyStrategy name should be "{expected}"') def step_temporal_name(context: Context, expected: str) -> None: assert context.temporal_strategy.name == expected @then('the TemporalArchaeologyStrategy explain should contain "{text}"') def step_temporal_explain(context: Context, text: str) -> None: explanation = context.temporal_strategy.explain() assert text in explanation # --------------------------------------------------------------------------- # PlanDecisionContextStrategy — When / Then # --------------------------------------------------------------------------- @when("I assemble with the PlanDecisionContextStrategy") def step_assemble_plan_decision(context: Context) -> None: context.plan_decision_result = list( context.plan_decision_strategy.assemble( context.strategy3_fragments, context.strategy3_budget, ) ) @when("I check can_handle on PlanDecisionContextStrategy") def step_check_plan_decision_can_handle(context: Context) -> None: context.plan_decision_confidence = context.plan_decision_strategy.can_handle({}) @then('the first plan_decision result should have uko_node "{expected}"') def step_plan_decision_first_node(context: Context, expected: str) -> None: assert len(context.plan_decision_result) >= 1 assert context.plan_decision_result[0].uko_node == expected, ( f"Expected {expected}, got {context.plan_decision_result[0].uko_node}" ) @then("{count:d} fragments should be returned by plan_decision strategy") def step_plan_decision_exact_count(context: Context, count: int) -> None: assert len(context.plan_decision_result) == count @then("at most {count:d} fragments should be returned by plan_decision strategy") def step_plan_decision_at_most_count(context: Context, count: int) -> None: assert len(context.plan_decision_result) <= count @then("the plan_decision confidence should be {expected:g}") def step_plan_decision_confidence(context: Context, expected: float) -> None: assert context.plan_decision_confidence == expected @then('the PlanDecisionContextStrategy name should be "{expected}"') def step_plan_decision_name(context: Context, expected: str) -> None: assert context.plan_decision_strategy.name == expected @then('the PlanDecisionContextStrategy explain should contain "{text}"') def step_plan_decision_explain(context: Context, text: str) -> None: explanation = context.plan_decision_strategy.explain() assert text in explanation # --------------------------------------------------------------------------- # Pipeline Registration — Given / When / Then # --------------------------------------------------------------------------- @given("an ACMS pipeline for phase3 strategy tests") def step_given_phase3_pipeline(context: Context) -> None: context.phase3_test_pipeline = ACMSPipeline() @when("I register ArceStrategy with the pipeline") def step_register_arce(context: Context) -> None: strategy = ArceStrategy() context.phase3_test_pipeline.register_strategy("arce", strategy) @when("I register all phase3 strategies with the pipeline") def step_register_all_phase3(context: Context) -> None: context.phase3_test_pipeline.register_strategy("arce", ArceStrategy()) context.phase3_test_pipeline.register_strategy( "temporal-archaeology", TemporalArchaeologyStrategy() ) context.phase3_test_pipeline.register_strategy( "plan-decision-context", PlanDecisionContextStrategy() ) @then('the phase3 pipeline should have strategy "{name}"') def step_phase3_pipeline_has_strategy(context: Context, name: str) -> None: assert name in context.phase3_test_pipeline._strategies # --------------------------------------------------------------------------- # Pipeline Integration — Given / When / Then # --------------------------------------------------------------------------- @given("the ACMS pipeline with a RelevanceCoherenceOrderer") def step_pipeline_with_orderer(context: Context) -> None: context.phase3_pipeline = ACMSPipeline( orderer=RelevanceCoherenceOrderer(), ) @given("the ACMS pipeline with a ProvenancePreambleGenerator") def step_pipeline_with_preamble(context: Context) -> None: context.phase3_pipeline = ACMSPipeline( preamble_generator=ProvenancePreambleGenerator(), ) @when("I assemble context through the phase3 pipeline") def step_assemble_phase3_pipeline(context: Context) -> None: budget = ContextBudget(max_tokens=100000, reserved_tokens=0) payload = context.phase3_pipeline.assemble( plan_id=_TEST_PLAN_ID, fragments=context.phase3_fragments, budget=budget, ) context.phase3_payload = payload @then("the phase3 pipeline output should contain {count:d} fragments") def step_phase3_pipeline_count_plural(context: Context, count: int) -> None: assert len(context.phase3_payload.fragments) == count, ( f"Expected {count}, got {len(context.phase3_payload.fragments)}" ) @then("the phase3 pipeline output should contain {count:d} fragment") def step_phase3_pipeline_count_singular(context: Context, count: int) -> None: assert len(context.phase3_payload.fragments) == count @then("the phase3 pipeline output should have a preamble") def step_phase3_pipeline_has_preamble(context: Context) -> None: assert context.phase3_payload.preamble is not None, ( "Expected pipeline output to have a preamble" ) @then('the phase3 pipeline preamble should contain "{text}"') def step_phase3_pipeline_preamble_contains(context: Context, text: str) -> None: preamble = context.phase3_payload.preamble assert preamble is not None, "Preamble was None" assert text in preamble, f"Expected preamble to contain '{text}', got:\n{preamble}" # --------------------------------------------------------------------------- # Internal helpers # --------------------------------------------------------------------------- def _extract_prefix(uko_node: str) -> str: """Extract grouping prefix for adjacency checks.""" if "://" in uko_node: uko_node = uko_node.split("://", 1)[1] segments = [s for s in uko_node.replace("\\", "/").split("/") if s] return "/".join(segments[:2]) if len(segments) >= 2 else uko_node