"""Step definitions for ``features/context_strategies.feature``. Covers the first batch of built-in context strategies: * **SimpleKeywordStrategy** — keyword matching / word-density fallback * **SemanticEmbeddingStrategy** — Jaccard word-overlap similarity * **BreadthDepthNavigatorStrategy** — UKO hierarchy navigation * **RelevanceScoringStrategy** — relevance scoring via embeddings and metadata Also covers pipeline registration of all four strategies. """ from __future__ import annotations from typing import Any from behave import given, then, when from behave.runner import Context from cleveragents.application.services.acms_service import ACMSPipeline from cleveragents.application.services.context_strategies import ( BreadthDepthNavigatorStrategy, RelevanceScoringStrategy, SemanticEmbeddingStrategy, SimpleKeywordStrategy, ) from cleveragents.domain.models.core.context_fragment import ( ContextBudget, ContextFragment, FragmentProvenance, ) __all__: list[str] = [] # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _DEFAULT_PROVENANCE = FragmentProvenance(resource_uri="test://default") def _make_fragment( uko_node: str, content: str, score: float, tokens: int, depth: int, ) -> ContextFragment: """Build a ``ContextFragment`` for test purposes.""" return ContextFragment( uko_node=uko_node, content=content, relevance_score=score, token_count=tokens, detail_depth=depth, provenance=FragmentProvenance(resource_uri=uko_node), ) # --------------------------------------------------------------------------- # Given steps # --------------------------------------------------------------------------- @given('a SimpleKeywordStrategy with query "{query}"') def step_simple_keyword_with_query(context: Context, query: str) -> None: strategy = SimpleKeywordStrategy() strategy.set_query(query) context.strategy = strategy @given("a SimpleKeywordStrategy without query") def step_simple_keyword_no_query(context: Context) -> None: context.strategy = SimpleKeywordStrategy() @given('a SemanticEmbeddingStrategy with query "{query}"') def step_semantic_embedding_with_query(context: Context, query: str) -> None: strategy = SemanticEmbeddingStrategy() strategy.set_query(query) context.strategy = strategy @given("a SemanticEmbeddingStrategy without query") def step_semantic_embedding_no_query(context: Context) -> None: context.strategy = SemanticEmbeddingStrategy() @given('a BreadthDepthNavigatorStrategy with focus "{focus}"') def step_breadth_depth_with_focus(context: Context, focus: str) -> None: strategy = BreadthDepthNavigatorStrategy() strategy.set_focus([focus]) context.strategy = strategy @given("a BreadthDepthNavigatorStrategy without focus") def step_breadth_depth_no_focus(context: Context) -> None: context.strategy = BreadthDepthNavigatorStrategy() @given("the following strategy fragments:") def step_strategy_fragments_table(context: Context) -> None: context.strategy_fragments = [] for row in context.table: frag = _make_fragment( uko_node=row["uko_node"], content=row["content"], score=float(row["score"]), tokens=int(row["tokens"]), depth=int(row["depth"]), ) context.strategy_fragments.append(frag) @given("an empty strategy fragment list") def step_empty_strategy_fragments(context: Context) -> None: context.strategy_fragments = [] @given( "a strategy budget with max_tokens {max_tokens:d} and reserved_tokens {reserved:d}" ) def step_strategy_budget(context: Context, max_tokens: int, reserved: int) -> None: context.strategy_budget = ContextBudget( max_tokens=max_tokens, reserved_tokens=reserved ) @given("an ACMS pipeline for strategy tests") def step_pipeline_for_strategies(context: Context) -> None: context.pipeline = ACMSPipeline() # --------------------------------------------------------------------------- # When steps # --------------------------------------------------------------------------- @when("I assemble with the SimpleKeywordStrategy") def step_assemble_simple_keyword(context: Context) -> None: context.strategy_result = list( context.strategy.assemble(context.strategy_fragments, context.strategy_budget) ) @when("I assemble with the SemanticEmbeddingStrategy") def step_assemble_semantic_embedding(context: Context) -> None: context.strategy_result = list( context.strategy.assemble(context.strategy_fragments, context.strategy_budget) ) @when("I assemble with the BreadthDepthNavigatorStrategy") def step_assemble_breadth_depth(context: Context) -> None: context.strategy_result = list( context.strategy.assemble(context.strategy_fragments, context.strategy_budget) ) @when('I check can_handle on SimpleKeywordStrategy with query "{query}"') def step_can_handle_simple_keyword(context: Context, query: str) -> None: request: dict[str, Any] = {"query": query} context.confidence = context.strategy.can_handle(request) @when('I check can_handle on SemanticEmbeddingStrategy with query "{query}"') def step_can_handle_semantic_with_query(context: Context, query: str) -> None: request: dict[str, Any] = {"query": query} context.confidence = context.strategy.can_handle(request) @when("I check can_handle on SemanticEmbeddingStrategy without query") def step_can_handle_semantic_no_query(context: Context) -> None: request: dict[str, Any] = {} context.confidence = context.strategy.can_handle(request) @when("I check can_handle on BreadthDepthNavigator with focus") def step_can_handle_breadth_depth_with_focus(context: Context) -> None: request: dict[str, Any] = {"focus": ["project://app/io.py"]} context.confidence = context.strategy.can_handle(request) @when("I check can_handle on BreadthDepthNavigator with string focus") def step_can_handle_breadth_depth_string_focus(context: Context) -> None: request: dict[str, Any] = {"focus": "project://app/io.py"} context.confidence = context.strategy.can_handle(request) @when("I check can_handle on BreadthDepthNavigator without focus") def step_can_handle_breadth_depth_no_focus(context: Context) -> None: request: dict[str, Any] = {} context.confidence = context.strategy.can_handle(request) @when("I register SimpleKeywordStrategy with the pipeline") def step_register_simple_keyword(context: Context) -> None: context.pipeline.register_strategy("simple-keyword", SimpleKeywordStrategy()) @when("I register all batch 1 strategies with the pipeline") def step_register_all_batch1(context: Context) -> None: context.pipeline.register_strategy("simple-keyword", SimpleKeywordStrategy()) context.pipeline.register_strategy( "semantic-embedding", SemanticEmbeddingStrategy() ) context.pipeline.register_strategy( "breadth-depth-navigator", BreadthDepthNavigatorStrategy() ) # --------------------------------------------------------------------------- # Then steps # --------------------------------------------------------------------------- @then('the first result fragment should have uko_node "{uko_node}"') def step_first_result_uko_node(context: Context, uko_node: str) -> None: assert len(context.strategy_result) > 0, "Expected at least one result fragment" actual = context.strategy_result[0].uko_node assert actual == uko_node, ( f"Expected first fragment uko_node '{uko_node}', got '{actual}'" ) @then("{count:d} fragments should be returned by strategy") def step_fragment_count(context: Context, count: int) -> None: actual = len(context.strategy_result) assert actual == count, f"Expected {count} fragments, got {actual}" @then("the strategy confidence should be {expected:g}") def step_strategy_confidence(context: Context, expected: float) -> None: actual = context.confidence assert abs(actual - expected) < 1e-6, ( f"Expected strategy confidence {expected}, got {actual}" ) @then("the SimpleKeywordStrategy should not support semantic search") def step_simple_keyword_no_semantic(context: Context) -> None: caps = context.strategy.capabilities assert not caps.supports_semantic_search, ( "SimpleKeywordStrategy should NOT support semantic search" ) @then('the SimpleKeywordStrategy name should be "{name}"') def step_simple_keyword_name(context: Context, name: str) -> None: actual = context.strategy.name assert actual == name, f"Expected name '{name}', got '{actual}'" @then("the SemanticEmbeddingStrategy should support semantic search") def step_semantic_supports_search(context: Context) -> None: caps = context.strategy.capabilities assert caps.supports_semantic_search, ( "SemanticEmbeddingStrategy should support semantic search" ) @then('the SemanticEmbeddingStrategy name should be "{name}"') def step_semantic_name(context: Context, name: str) -> None: actual = context.strategy.name assert actual == name, f"Expected name '{name}', got '{actual}'" @then("the BreadthDepthNavigatorStrategy should support graph navigation") def step_breadth_depth_supports_graph(context: Context) -> None: caps = context.strategy.capabilities assert caps.supports_graph_navigation, ( "BreadthDepthNavigatorStrategy should support graph navigation" ) @then('the BreadthDepthNavigatorStrategy name should be "{name}"') def step_breadth_depth_name(context: Context, name: str) -> None: actual = context.strategy.name assert actual == name, f"Expected name '{name}', got '{actual}'" @then('the SimpleKeywordStrategy explain should contain "{text}"') def step_simple_keyword_explain(context: Context, text: str) -> None: explanation = context.strategy.explain() assert text.lower() in explanation.lower(), ( f"Expected explain to contain '{text}', got: {explanation}" ) @then('the SemanticEmbeddingStrategy explain should contain "{text}"') def step_semantic_explain(context: Context, text: str) -> None: explanation = context.strategy.explain() assert text.lower() in explanation.lower(), ( f"Expected explain to contain '{text}', got: {explanation}" ) @then('the BreadthDepthNavigatorStrategy explain should contain "{text}"') def step_breadth_depth_explain(context: Context, text: str) -> None: explanation = context.strategy.explain() assert text.lower() in explanation.lower(), ( f"Expected explain to contain '{text}', got: {explanation}" ) @then('the pipeline should have strategy "{name}"') def step_pipeline_has_strategy(context: Context, name: str) -> None: strategies = context.pipeline.BUILTIN_STRATEGIES registered = context.pipeline._strategies has_builtin = name in strategies has_registered = name in registered assert has_builtin or has_registered, ( f"Pipeline does not have strategy '{name}'. " f"Builtins: {list(strategies.keys())}, Registered: {list(registered.keys())}" ) # =========================================================================== # RelevanceScoringStrategy Steps # =========================================================================== @given('a RelevanceScoringStrategy with query "{query}"') def step_relevance_scoring_with_query(context: Context, query: str) -> None: """Create a RelevanceScoringStrategy with a query.""" context.strategy = RelevanceScoringStrategy() context.strategy.set_query(query) @given("a RelevanceScoringStrategy without query") def step_relevance_scoring_without_query(context: Context) -> None: """Create a RelevanceScoringStrategy without a query.""" context.strategy = RelevanceScoringStrategy() @given( "a RelevanceScoringStrategy with similarity_weight {sim_weight} " "and recency_weight {rec_weight} and importance_weight {imp_weight}" ) def step_relevance_scoring_with_weights( context: Context, sim_weight: str, rec_weight: str, imp_weight: str, ) -> None: """Create a RelevanceScoringStrategy with custom weights.""" context.strategy = RelevanceScoringStrategy( similarity_weight=float(sim_weight), recency_weight=float(rec_weight), importance_weight=float(imp_weight), ) @when("I assemble with the RelevanceScoringStrategy") def step_assemble_with_relevance_scoring(context: Context) -> None: """Assemble fragments using RelevanceScoringStrategy.""" result = context.strategy.assemble( context.strategy_fragments, context.strategy_budget ) context.strategy_result = list(result) @when('I check can_handle on RelevanceScoringStrategy with query "{query}"') def step_check_can_handle_relevance_scoring_with_query( context: Context, query: str ) -> None: """Check can_handle for RelevanceScoringStrategy with a query.""" request: dict[str, Any] = {"query": query} context.confidence = context.strategy.can_handle(request) @when("I check can_handle on RelevanceScoringStrategy without query") def step_check_can_handle_relevance_scoring_without_query(context: Context) -> None: """Check can_handle for RelevanceScoringStrategy without a query.""" request: dict[str, Any] = {} context.confidence = context.strategy.can_handle(request) @then("the RelevanceScoringStrategy should support semantic search") def step_relevance_scoring_supports_semantic(context: Context) -> None: """Verify RelevanceScoringStrategy supports semantic search.""" assert context.strategy.capabilities.supports_semantic_search is True @then('the RelevanceScoringStrategy name should be "{expected_name}"') def step_relevance_scoring_name(context: Context, expected_name: str) -> None: """Verify RelevanceScoringStrategy name.""" assert context.strategy.name == expected_name @then('the RelevanceScoringStrategy explain should contain "{text}"') def step_relevance_scoring_explain(context: Context, text: str) -> None: """Verify RelevanceScoringStrategy explain contains text.""" explanation = context.strategy.explain() assert text.lower() in explanation.lower(), ( f"Expected explain to contain '{text}', got: {explanation}" ) @when("I register RelevanceScoringStrategy with the pipeline") def step_register_relevance_scoring_strategy(context: Context) -> None: """Register RelevanceScoringStrategy with the pipeline.""" strategy = RelevanceScoringStrategy() context.pipeline.register_strategy("relevance-scoring", strategy)