"""Step definitions for the CRP Domain Models feature.""" from __future__ import annotations from typing import Any from behave import given, then, when from pydantic import ValidationError from cleveragents.domain.models.acms.crp import ( AssembledContext, ContextBudget, ContextFragment, ContextRequest, DetailLevelMap, FragmentProvenance, ) from cleveragents.domain.models.core.project import TemporalScope from cleveragents.skills.builtins.context_ops import ( _handle_get_context_budget, _handle_query_history, _handle_request_context, build_context_skill_definition, register_skill_context_tools, ) from cleveragents.tool.registry import ToolRegistry __all__: list[str] = [] # --------------------------------------------------------------------------- # Helper # --------------------------------------------------------------------------- def _make_provenance( resource_uri: str = "uko-py:module/test", ) -> FragmentProvenance: """Create a simple provenance for test fragments.""" return FragmentProvenance(resource_uri=resource_uri) # --------------------------------------------------------------------------- # DetailLevelMap # --------------------------------------------------------------------------- @given('a DetailLevelMap for domain "{domain}" with max depth {max_depth:d}') def step_given_detail_level_map(context: Any, domain: str, max_depth: int) -> None: context.detail_level_map = DetailLevelMap(domain=domain, max_depth=max_depth) @given('the map has level "{name}" at depth {depth:d}') def step_given_map_has_level(context: Any, name: str, depth: int) -> None: context.detail_level_map.register(name, depth) @then('the map should resolve "{name}" to {expected:d}') def step_then_map_resolve_name(context: Any, name: str, expected: int) -> None: actual: int = context.detail_level_map.resolve(name) assert actual == expected, f"Expected {expected}, got {actual}" @then("the map should resolve integer {value:d} to {expected:d}") def step_then_map_resolve_int(context: Any, value: int, expected: int) -> None: actual: int = context.detail_level_map.resolve(value) assert actual == expected, f"Expected {expected}, got {actual}" @given('a parent DetailLevelMap for domain "{domain}" with max depth {max_depth:d}') def step_given_parent_map(context: Any, domain: str, max_depth: int) -> None: context.parent_detail_level_map = DetailLevelMap(domain=domain, max_depth=max_depth) @given('the parent map has level "{name}" at depth {depth:d}') def step_given_parent_level(context: Any, name: str, depth: int) -> None: context.parent_detail_level_map.register(name, depth) @given('a child DetailLevelMap for domain "{domain}" with max depth {max_depth:d}') def step_given_child_map(context: Any, domain: str, max_depth: int) -> None: context.child_detail_level_map = DetailLevelMap( domain=domain, max_depth=max_depth, parent=context.parent_detail_level_map, ) @given('the child map has level "{name}" at depth {depth:d}') def step_given_child_level(context: Any, name: str, depth: int) -> None: context.child_detail_level_map.register(name, depth) @then('the child map should resolve "{name}" to {expected:d}') def step_then_child_resolve(context: Any, name: str, expected: int) -> None: actual: int = context.child_detail_level_map.resolve(name) assert actual == expected, f"Expected {expected}, got {actual}" @then('resolving "{name}" should raise ValueError') def step_then_resolve_raises(context: Any, name: str) -> None: try: context.detail_level_map.resolve(name) raise AssertionError(f"Expected ValueError for '{name}'") except ValueError: pass @when('I register level "{name}" at depth {depth:d}') def step_when_register_level(context: Any, name: str, depth: int) -> None: context.detail_level_map.register(name, depth) @then( 'creating a DetailLevelMap with level "{name}" at depth {depth:d} ' "should raise ValueError" ) def step_then_bad_level_raises(context: Any, name: str, depth: int) -> None: try: DetailLevelMap(domain="test:", max_depth=9, levels={name: depth}) raise AssertionError("Expected ValueError") except (ValueError, ValidationError): pass @then('registering level "{name}" at depth {depth:d} should raise ValueError') def step_then_register_bad_raises(context: Any, name: str, depth: int) -> None: try: context.detail_level_map.register(name, depth) raise AssertionError("Expected ValueError") except ValueError: pass @given( 'a DetailLevelMap for domain "{domain}" with max depth {max_depth:d} ' 'and levels "{name}" at {depth:d}' ) def step_given_detail_level_map_with_levels( context: Any, domain: str, max_depth: int, name: str, depth: int ) -> None: context.detail_level_map = DetailLevelMap( domain=domain, max_depth=max_depth, levels={name: depth} ) @then('the child effective_levels should contain "{name}" at {expected:d}') def step_then_effective_levels(context: Any, name: str, expected: int) -> None: levels: dict[str, int] = context.child_detail_level_map.effective_levels() assert name in levels, f"'{name}' not in effective_levels" assert levels[name] == expected, ( f"Expected {expected} for '{name}', got {levels[name]}" ) # --------------------------------------------------------------------------- # FragmentProvenance # --------------------------------------------------------------------------- @given('a FragmentProvenance with resource URI "{uri}"') def step_given_provenance(context: Any, uri: str) -> None: context.provenance = FragmentProvenance(resource_uri=uri) @then('the provenance resource_uri should be "{expected}"') def step_then_provenance_uri(context: Any, expected: str) -> None: assert context.provenance.resource_uri == expected @then("the provenance location should be empty") def step_then_provenance_location_empty(context: Any) -> None: assert context.provenance.location == "" @then("the provenance strategy should be empty") def step_then_provenance_strategy_empty(context: Any) -> None: assert context.provenance.strategy == "" @then("creating a FragmentProvenance with empty resource_uri should raise ValueError") def step_then_provenance_empty_uri(context: Any) -> None: try: FragmentProvenance(resource_uri="") raise AssertionError("Expected ValueError") except (ValueError, ValidationError): pass # --------------------------------------------------------------------------- # ContextBudget # --------------------------------------------------------------------------- @given("a ContextBudget with max_tokens {max_tok:d} and reserved_tokens {res:d}") def step_given_budget(context: Any, max_tok: int, res: int) -> None: context.budget = ContextBudget(max_tokens=max_tok, reserved_tokens=res) @then("the budget available_tokens should be {expected:d}") def step_then_budget_available(context: Any, expected: int) -> None: assert context.budget.available_tokens == expected @then("the budget max_tokens should be {expected:d}") def step_then_budget_max(context: Any, expected: int) -> None: assert context.budget.max_tokens == expected @then("the budget reserved_tokens should be {expected:d}") def step_then_budget_reserved(context: Any, expected: int) -> None: assert context.budget.reserved_tokens == expected @then("creating a ContextBudget with max_tokens {max_tok:d} should raise ValueError") def step_then_budget_bad_max(context: Any, max_tok: int) -> None: try: ContextBudget(max_tokens=max_tok) raise AssertionError("Expected ValueError") except (ValueError, ValidationError): pass @then( "creating a ContextBudget with max_tokens {max_tok:d} " "and reserved_tokens {res:d} should raise ValueError" ) def step_then_budget_bad_reserved(context: Any, max_tok: int, res: int) -> None: try: ContextBudget(max_tokens=max_tok, reserved_tokens=res) raise AssertionError("Expected ValueError") except (ValueError, ValidationError): pass # --------------------------------------------------------------------------- # ContextFragment # --------------------------------------------------------------------------- @given('a ContextFragment with uko_node "{node}" and content "{content}"') def step_given_fragment(context: Any, node: str, content: str) -> None: context.fragment_node = node context.fragment_content = content context.fragment_detail_depth = 0 context.fragment_token_count = 0 context.fragment_relevance = 0.5 @given("the fragment has detail_depth {depth:d} and token_count {tokens:d}") def step_given_fragment_depth_tokens(context: Any, depth: int, tokens: int) -> None: context.fragment_detail_depth = depth context.fragment_token_count = tokens @given("the fragment has relevance_score {score:g}") def step_given_fragment_relevance(context: Any, score: float) -> None: context.fragment_relevance = score context.fragment = ContextFragment( uko_node=context.fragment_node, content=context.fragment_content, detail_depth=context.fragment_detail_depth, token_count=context.fragment_token_count, relevance_score=context.fragment_relevance, provenance=_make_provenance(), ) @then('the fragment uko_node should be "{expected}"') def step_then_fragment_node(context: Any, expected: str) -> None: assert context.fragment.uko_node == expected @then("the fragment token_count should be {expected:d}") def step_then_fragment_tokens(context: Any, expected: int) -> None: assert context.fragment.token_count == expected @then("the fragment relevance_score should be {expected:g}") def step_then_fragment_relevance(context: Any, expected: float) -> None: assert context.fragment.relevance_score == expected @then("the fragment detail_depth should be {expected:d}") def step_then_fragment_depth(context: Any, expected: int) -> None: assert context.fragment.detail_depth == expected @then("creating a ContextFragment with token_count {count:d} should raise ValueError") def step_then_fragment_bad_tokens(context: Any, count: int) -> None: try: ContextFragment( uko_node="uko-py:test", content="test", detail_depth=0, token_count=count, relevance_score=0.5, provenance=_make_provenance(), ) raise AssertionError("Expected ValueError") except (ValueError, ValidationError): pass @then( "creating a ContextFragment with relevance_score {score:g} should raise ValueError" ) def step_then_fragment_bad_relevance(context: Any, score: float) -> None: try: ContextFragment( uko_node="uko-py:test", content="test", detail_depth=0, token_count=10, relevance_score=score, provenance=_make_provenance(), ) raise AssertionError("Expected ValueError") except (ValueError, ValidationError): pass @then("creating a ContextFragment with detail_depth {depth:d} should raise ValueError") def step_then_fragment_bad_depth(context: Any, depth: int) -> None: try: ContextFragment( uko_node="uko-py:test", content="test", detail_depth=depth, token_count=10, relevance_score=0.5, provenance=_make_provenance(), ) raise AssertionError("Expected ValueError") except (ValueError, ValidationError): pass @then("creating a ContextFragment with empty uko_node should raise ValueError") def step_then_fragment_empty_node(context: Any) -> None: try: ContextFragment( uko_node="", content="test", detail_depth=0, token_count=10, relevance_score=0.5, provenance=_make_provenance(), ) raise AssertionError("Expected ValueError") except (ValueError, ValidationError): pass # --------------------------------------------------------------------------- # ContextRequest # --------------------------------------------------------------------------- @given('a ContextRequest with purpose "{purpose}"') def step_given_request(context: Any, purpose: str) -> None: context.ctx_request = ContextRequest(purpose=purpose) @given('a full ContextRequest with query "{query}" and focus "{focus}"') def step_given_full_request(context: Any, query: str, focus: str) -> None: context.ctx_request = ContextRequest( query=query, focus=[focus], ) @given('the request has breadth {breadth:d} and depth "{depth}"') def step_given_request_breadth_depth_str( context: Any, breadth: int, depth: str ) -> None: context.ctx_request = context.ctx_request.model_copy( update={"breadth": breadth, "depth": depth} ) @given('the request has priority {priority:g} and purpose "{purpose}"') def step_given_request_priority_purpose( context: Any, priority: float, purpose: str ) -> None: context.ctx_request = context.ctx_request.model_copy( update={"priority": priority, "purpose": purpose} ) @given('a ContextRequest with depth "{depth}"') def step_given_request_depth_str(context: Any, depth: str) -> None: context.ctx_request = ContextRequest(depth=depth) @given("a ContextRequest with breadth {breadth:d}") def step_given_request_breadth(context: Any, breadth: int) -> None: context.ctx_request = ContextRequest(breadth=breadth) @given("a ContextRequest with max_tokens {max_tok:d}") def step_given_request_max_tokens(context: Any, max_tok: int) -> None: context.ctx_request = ContextRequest(max_tokens=max_tok) @then('the request purpose should be "{expected}"') def step_then_request_purpose(context: Any, expected: str) -> None: assert context.ctx_request.purpose == expected @then("the request breadth should be {expected:d}") def step_then_request_breadth(context: Any, expected: int) -> None: assert context.ctx_request.breadth == expected @then("the request depth should be {expected:d}") def step_then_request_depth_int(context: Any, expected: int) -> None: assert context.ctx_request.depth == expected @then('the request depth should be "{expected}"') def step_then_request_depth_str(context: Any, expected: str) -> None: assert context.ctx_request.depth == expected @then("the request depth_gradient should be true") def step_then_request_gradient_true(context: Any) -> None: assert context.ctx_request.depth_gradient is True @then("the request priority should be {expected:g}") def step_then_request_priority(context: Any, expected: float) -> None: assert context.ctx_request.priority == expected @then('the request temporal should be "{expected}"') def step_then_request_temporal(context: Any, expected: str) -> None: assert context.ctx_request.temporal == TemporalScope(expected) @then("the request query should be None") def step_then_request_query_none(context: Any) -> None: assert context.ctx_request.query is None @then('the request query should be "{expected}"') def step_then_request_query(context: Any, expected: str) -> None: assert context.ctx_request.query == expected @then("the request max_tokens should be None") def step_then_request_max_tokens_none(context: Any) -> None: assert context.ctx_request.max_tokens is None @then("the request max_tokens should be {expected:d}") def step_then_request_max_tokens(context: Any, expected: int) -> None: assert context.ctx_request.max_tokens == expected @then("creating a ContextRequest with breadth {breadth:d} should raise ValueError") def step_then_request_bad_breadth(context: Any, breadth: int) -> None: try: ContextRequest(breadth=breadth) raise AssertionError("Expected ValueError") except (ValueError, ValidationError): pass @then("creating a ContextRequest with depth {depth:d} should raise ValueError") def step_then_request_bad_depth(context: Any, depth: int) -> None: try: ContextRequest(depth=depth) raise AssertionError("Expected ValueError") except (ValueError, ValidationError): pass @then("creating a ContextRequest with priority {priority:g} should raise ValueError") def step_then_request_bad_priority(context: Any, priority: float) -> None: try: ContextRequest(priority=priority) raise AssertionError("Expected ValueError") except (ValueError, ValidationError): pass @then("creating a ContextRequest with max_tokens {max_tok:d} should raise ValueError") def step_then_request_bad_max_tokens(context: Any, max_tok: int) -> None: try: ContextRequest(max_tokens=max_tok) raise AssertionError("Expected ValueError") except (ValueError, ValidationError): pass # --------------------------------------------------------------------------- # AssembledContext # --------------------------------------------------------------------------- @given("an AssembledContext with total_tokens {total:d} and budget_used {used:g}") def step_given_assembled(context: Any, total: int, used: float) -> None: context.assembled_total = total context.assembled_used = used @given('the assembled context has hash "{hash_val}"') def step_given_assembled_hash(context: Any, hash_val: str) -> None: context.assembled = AssembledContext( total_tokens=context.assembled_total, budget_used=context.assembled_used, context_hash=hash_val, ) @then("the assembled total_tokens should be {expected:d}") def step_then_assembled_total(context: Any, expected: int) -> None: assert context.assembled.total_tokens == expected @then("the assembled budget_used should be {expected:g}") def step_then_assembled_used(context: Any, expected: float) -> None: assert context.assembled.budget_used == expected @then('the assembled context_hash should be "{expected}"') def step_then_assembled_hash(context: Any, expected: str) -> None: assert context.assembled.context_hash == expected @then("creating an AssembledContext with budget_used {used:g} should raise ValueError") def step_then_assembled_bad_used(context: Any, used: float) -> None: try: AssembledContext( total_tokens=100, budget_used=used, context_hash="test", ) raise AssertionError("Expected ValueError") except (ValueError, ValidationError): pass @then( "creating an AssembledContext with total_tokens {total:d} should raise ValueError" ) def step_then_assembled_bad_total(context: Any, total: int) -> None: try: AssembledContext( total_tokens=total, budget_used=0.5, context_hash="test", ) raise AssertionError("Expected ValueError") except (ValueError, ValidationError): pass # --------------------------------------------------------------------------- # Context skill tools (wired to ACMS pipeline) # --------------------------------------------------------------------------- def _setup_di_overrides(tier_service: Any) -> None: """Override DI providers for test isolation.""" from cleveragents.application.container import get_container, reset_container from cleveragents.application.services.acms_service import ACMSPipeline reset_container() container = get_container() from dependency_injector import providers container.context_tier_service.override(providers.Object(tier_service)) container.acms_pipeline.override(providers.Object(ACMSPipeline())) def _teardown_di_overrides() -> None: """Reset DI container after test.""" from cleveragents.application.container import reset_container reset_container() def _make_tier_service_with_fragments() -> Any: """Create a ContextTierService pre-loaded with sample fragments.""" from cleveragents.application.services.context_tiers import ContextTierService from cleveragents.domain.models.acms.tiers import ContextTier, TieredFragment svc = ContextTierService() svc.store( TieredFragment( fragment_id="frag-auth-001", content="class AuthManager: handles authentication", tier=ContextTier.HOT, resource_id="uko-py:module/auth", token_count=50, ) ) svc.store( TieredFragment( fragment_id="frag-db-002", content="class DatabaseService: handles persistence", tier=ContextTier.WARM, resource_id="uko-py:module/database", token_count=45, ) ) svc.store( TieredFragment( fragment_id="frag-api-003", content="class APIRouter: routes HTTP requests", tier=ContextTier.COLD, resource_id="uko-py:module/api", token_count=40, ) ) return svc @given("a context tier service with sample fragments") def step_given_tier_service_with_fragments(context: Any) -> None: tier_svc = _make_tier_service_with_fragments() _setup_di_overrides(tier_svc) context.add_cleanup(_teardown_di_overrides) @given("an empty context tier service") def step_given_empty_tier_service(context: Any) -> None: from cleveragents.application.services.context_tiers import ContextTierService tier_svc = ContextTierService() _setup_di_overrides(tier_svc) context.add_cleanup(_teardown_di_overrides) @when('I call the request_context tool with purpose "{purpose}"') def step_when_call_request_context(context: Any, purpose: str) -> None: context.request_context_result = _handle_request_context({"purpose": purpose}) @when('I call the request_context tool filtering by query "{query}"') def step_when_call_request_context_with_query(context: Any, query: str) -> None: context.request_context_result = _handle_request_context( {"purpose": "filtered query", "query": query} ) @when('I call the request_context tool with focus on "{focus}"') def step_when_call_request_context_with_focus(context: Any, focus: str) -> None: context.request_context_result = _handle_request_context( {"purpose": "focused query", "focus": [focus]} ) @then('the request_context result should contain "{key}" key') def step_then_request_context_has_key(context: Any, key: str) -> None: assert key in context.request_context_result, ( f"Key '{key}' not in result: {context.request_context_result}" ) @then('the request_context result "{key}" should be an empty list') def step_then_request_context_empty_list(context: Any, key: str) -> None: assert context.request_context_result[key] == [], ( f"Expected empty list, got {context.request_context_result[key]}" ) @then('the request_context result "{key}" should be {value:d}') def step_then_request_context_int_value(context: Any, key: str, value: int) -> None: assert context.request_context_result[key] == value, ( f"Expected {value}, got {context.request_context_result[key]}" ) @then('the request_context result "fragments" should only contain matching content') def step_then_request_context_filtered(context: Any) -> None: fragments = context.request_context_result["fragments"] for frag in fragments: content_lower = frag["content"].lower() node_lower = frag.get("uko_node", "").lower() assert "auth" in content_lower or "auth" in node_lower, ( f"Fragment does not match query 'auth': {frag}" ) @when('I call the query_history tool with query "{query}"') def step_when_call_query_history(context: Any, query: str) -> None: context.query_history_result = _handle_query_history({"query": query}) @then('the query_history result should contain "{key}" key') def step_then_query_history_has_key(context: Any, key: str) -> None: assert key in context.query_history_result, ( f"Key '{key}' not in result: {context.query_history_result}" ) @then('the query_history result "{key}" should not be empty') def step_then_query_history_not_empty(context: Any, key: str) -> None: assert len(context.query_history_result[key]) > 0, ( f"Expected non-empty list for '{key}'" ) @then('the query_history result "{key}" should be an empty list') def step_then_query_history_empty_list(context: Any, key: str) -> None: assert context.query_history_result[key] == [], ( f"Expected empty list, got {context.query_history_result[key]}" ) @when("I call the get_context_budget tool") def step_when_call_get_budget(context: Any) -> None: context.budget_result = _handle_get_context_budget({}) @then('the budget result should contain "{key}" key') def step_then_budget_has_key(context: Any, key: str) -> None: assert key in context.budget_result, ( f"Key '{key}' not in result: {context.budget_result}" ) @then('the budget result "{key}" should be {value:d}') def step_then_budget_int_value(context: Any, key: str, value: int) -> None: assert context.budget_result[key] == value, ( f"Expected {value}, got {context.budget_result[key]}" ) @then('the budget result "{key}" should be greater than {value:d}') def step_then_budget_greater_than(context: Any, key: str, value: int) -> None: assert context.budget_result[key] > value, ( f"Expected > {value}, got {context.budget_result[key]}" ) @when("I build the builtin/context SkillDefinition") def step_when_build_skill_definition(context: Any) -> None: context.context_skill_def = build_context_skill_definition() @then('the skill definition name should be "{name}"') def step_then_skill_def_name(context: Any, name: str) -> None: assert context.context_skill_def.skill.name == name, ( f"Expected name '{name}', got '{context.context_skill_def.skill.name}'" ) @then("the skill definition should have {count:d} resolved tools") def step_then_skill_def_tool_count(context: Any, count: int) -> None: actual = len(context.context_skill_def.resolved_tools) assert actual == count, f"Expected {count} tools, got {actual}" @then("the skill definition should be read-only") def step_then_skill_def_read_only(context: Any) -> None: assert context.context_skill_def.metadata.read_only is True, ( "Expected skill definition to be read-only" ) @given("a CRP tool registry with context tools registered") def step_given_crp_registry_with_context(context: Any) -> None: context.crp_tool_registry = ToolRegistry() register_skill_context_tools(context.crp_tool_registry) @then('the CRP registry should contain tool "{name}"') def step_then_crp_registry_has_tool(context: Any, name: str) -> None: spec = context.crp_tool_registry.get(name) assert spec is not None, f"Tool '{name}' not found in CRP registry" @then('CRP tool "{name}" should have read_only capability') def step_then_crp_tool_read_only(context: Any, name: str) -> None: spec = context.crp_tool_registry.get(name) assert spec is not None, f"Tool '{name}' not found" assert spec.capabilities.read_only is True, f"Tool '{name}' is not read-only" @then('CRP tool "{name}" should require "{field}" in input schema') def step_then_crp_tool_requires_field(context: Any, name: str, field: str) -> None: spec = context.crp_tool_registry.get(name) assert spec is not None, f"Tool '{name}' not found" required: list[str] = spec.input_schema.get("required", []) assert field in required, f"Field '{field}' not in required fields: {required}"