diff --git a/features/context_policy_strategy_config.feature b/features/context_policy_strategy_config.feature new file mode 100644 index 000000000..7a52088d8 --- /dev/null +++ b/features/context_policy_strategy_config.feature @@ -0,0 +1,61 @@ +@context_policy @strategy_configuration +Feature: Context Policy Strategy Configuration + As a CleverAgents developer + I want to configure context assembly strategies in context policy YAML + So that I can control how context is assembled during ACMS phases + + Scenario: Create context policy with basic strategy + Given a context policy with strategy "basic" + When I validate the context policy + Then the policy strategy should be "basic" + And the policy should be valid + + Scenario: Create context policy with semantic strategy + Given a context policy with strategy "semantic" + When I validate the context policy + Then the policy strategy should be "semantic" + And the policy should be valid + + Scenario: Create context policy with relevance_scoring strategy + Given a context policy with strategy "relevance_scoring" + When I validate the context policy + Then the policy strategy should be "relevance_scoring" + And the policy should be valid + + Scenario: Create context policy with adaptive strategy + Given a context policy with strategy "adaptive" + When I validate the context policy + Then the policy strategy should be "adaptive" + And the policy should be valid + + Scenario: Create context policy with fusion strategy + Given a context policy with strategy "fusion" + When I validate the context policy + Then the policy strategy should be "fusion" + And the policy should be valid + + Scenario: Reject invalid strategy name + Given a context policy with strategy "invalid_strategy" + When I validate the context policy + Then the policy should be invalid + And the strategy error should mention "Invalid strategy" + + Scenario: Create context policy with strategy config + Given a context policy with strategy "semantic" + And strategy config with parameter "threshold" set to 0.5 + When I validate the context policy + Then the strategy_config should contain "threshold" + And the strategy_config["threshold"] should be 0.5 + + Scenario: Create context policy without strategy + Given a context policy without strategy + When I validate the context policy + Then the policy strategy should be None + And the policy should be valid + + Scenario: Create context policy with strategy but no config + Given a context policy with strategy "basic" + And no strategy config + When I validate the context policy + Then the strategy_config should be None + And the policy should be valid diff --git a/features/steps/context_policy_strategy_config_steps.py b/features/steps/context_policy_strategy_config_steps.py new file mode 100644 index 000000000..6681f6c94 --- /dev/null +++ b/features/steps/context_policy_strategy_config_steps.py @@ -0,0 +1,125 @@ +"""Step definitions for ProjectContextPolicy strategy configuration tests.""" + +from __future__ import annotations + +from behave import given, then, when +from behave.runner import Context +from pydantic import ValidationError + +from cleveragents.domain.models.core.context_policy import ( + ProjectContextPolicy, +) + +# ------------------------------------------------------------------- +# Strategy field support +# ------------------------------------------------------------------- + + +@given('a context policy with strategy "{strategy}"') +def step_policy_with_strategy(context: Context, strategy: str) -> None: + context.strategy = strategy + context.strategy_config = None + context.policy_error = None + + +@given("a context policy without strategy") +def step_policy_without_strategy(context: Context) -> None: + context.strategy = None + context.strategy_config = None + context.policy_error = None + + +@given('strategy config with parameter "{key}" set to {value}') +def step_add_strategy_config(context: Context, key: str, value: str) -> None: + if context.strategy_config is None: + context.strategy_config = {} + # Parse the value + try: + # Try to parse as float + context.strategy_config[key] = float(value) + except ValueError: + try: + # Try to parse as int + context.strategy_config[key] = int(value) + except ValueError: + # Keep as string + context.strategy_config[key] = value + + +@given("no strategy config") +def step_no_strategy_config(context: Context) -> None: + context.strategy_config = None + + +@when("I validate the context policy") +def step_validate_policy(context: Context) -> None: + context.policy_error = None + try: + context.policy = ProjectContextPolicy( + strategy=context.strategy, + strategy_config=context.strategy_config, + ) + except ValidationError as exc: + context.policy_error = str(exc) + + +@then('the policy strategy should be "{strategy}"') +def step_strategy_is(context: Context, strategy: str) -> None: + if strategy == "None": + assert context.policy.strategy is None + else: + assert context.policy.strategy == strategy + + +@then("the policy strategy should be None") +def step_strategy_is_none(context: Context) -> None: + assert context.policy.strategy is None + + +@then("the policy should be valid") +def step_policy_valid(context: Context) -> None: + assert context.policy_error is None, ( + f"Expected valid policy but got error: {context.policy_error}" + ) + + +@then("the policy should be invalid") +def step_policy_invalid(context: Context) -> None: + assert context.policy_error is not None, "Expected invalid policy but it was valid" + + +@then('the strategy error should mention "{text}"') +def step_error_mentions(context: Context, text: str) -> None: + assert text in context.policy_error, ( + f"Expected '{text}' in error: {context.policy_error}" + ) + + +# ------------------------------------------------------------------- +# Strategy configuration parameters +# ------------------------------------------------------------------- + + +@then('the strategy_config should contain "{key}"') +def step_strategy_config_contains(context: Context, key: str) -> None: + assert context.policy.strategy_config is not None + assert key in context.policy.strategy_config + + +@then('the strategy_config["{key}"] should be {value}') +def step_strategy_config_value(context: Context, key: str, value: str) -> None: + assert context.policy.strategy_config is not None + # Parse the expected value + try: + expected = float(value) + except ValueError: + try: + expected = int(value) + except ValueError: + expected = value + assert context.policy.strategy_config[key] == expected + + +@then("the strategy_config should be None") +def step_strategy_config_none(context: Context) -> None: + assert context.policy.strategy_config is None diff --git a/scripts/update_context_policy.py b/scripts/update_context_policy.py new file mode 100644 index 000000000..c2f6f548f --- /dev/null +++ b/scripts/update_context_policy.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Update context_policy.py with strategy configuration support.""" + +import sys +from pathlib import Path + +# Read the file +policy_file = Path("src/cleveragents/domain/models/core/context_policy.py") +content = policy_file.read_text() + +# Update the imports to include Any +content = content.replace( + "from typing import TYPE_CHECKING", "from typing import TYPE_CHECKING, Any" +) + +# Add VALID_STRATEGIES constant after VALID_PHASES +old_phases = ( + "VALID_PHASES: frozenset[str] = frozenset(" + '{"default", "strategize", "execute", "apply"})' +) +new_phases = ( + "VALID_PHASES: frozenset[str] = frozenset(" + '{"default", "strategize", "execute", "apply"})\n\n' + "VALID_STRATEGIES: frozenset[str] = frozenset({\n" + ' "basic",\n' + ' "semantic",\n' + ' "relevance_scoring",\n' + ' "adaptive",\n' + ' "fusion",\n' + "})" +) +content = content.replace(old_phases, new_phases) + +# Update the docstring for ProjectContextPolicy to mention strategy +old_docstring = ( + "class ProjectContextPolicy(BaseModel):\n" + ' """Controls what context is available during each ACMS phase.\n' + "\n" + " Uses view inheritance: ``default`` → ``strategize`` →\n" + " ``execute`` → ``apply``. Each phase can override or inherit\n" + " from its parent.\n" + "\n" + " An empty ``ProjectContextPolicy()`` defaults to including\n" + " everything (the ``default_view`` has empty include lists which\n" + ' means "all").\n' + ' """' +) + +new_docstring = ( + "class ProjectContextPolicy(BaseModel):\n" + ' """Controls what context is available during each ACMS phase.\n' + "\n" + " Uses view inheritance: ``default`` → ``strategize`` →\n" + " ``execute`` → ``apply``. Each phase can override or inherit\n" + " from its parent.\n" + "\n" + " An empty ``ProjectContextPolicy()`` defaults to including\n" + " everything (the ``default_view`` has empty include lists which\n" + ' means "all").\n' + "\n" + " Optionally specifies a context assembly strategy and its\n" + " configuration parameters.\n" + ' """' +) + +content = content.replace(old_docstring, new_docstring) + +# Add strategy and strategy_config fields before the resolve_view method +old_fields = ( + " apply_view: ContextView | None = Field(\n" + " default=None,\n" + ' description=("Overrides for Apply (inherits from execute if None)"),\n' + " )\n" + "\n" + " def resolve_view(self, phase: str) -> ContextView:" +) + +error_msg = "f\"Invalid strategy '{v}': must be one of {sorted(VALID_STRATEGIES)}\"" + +new_fields = ( + " apply_view: ContextView | None = Field(\n" + " default=None,\n" + ' description=("Overrides for Apply (inherits from execute if None)"),\n' + " )\n" + " strategy: str | None = Field(\n" + " default=None,\n" + " description=(\n" + ' "Context assembly strategy name. "\n' + ' "Valid values: basic, semantic, relevance_scoring, adaptive, fusion"\n' + " ),\n" + " )\n" + " strategy_config: dict[str, Any] | None = Field(\n" + " default=None,\n" + ' description="Strategy-specific configuration parameters",\n' + " )\n" + "\n" + ' @field_validator("strategy")\n' + " @classmethod\n" + " def _validate_strategy(\n" + " cls: type[ProjectContextPolicy],\n" + " v: str | None,\n" + " ) -> str | None:\n" + ' """Validate that strategy name is in the list of valid strategies."""\n' + " if v is not None and v not in VALID_STRATEGIES:\n" + " raise ValueError(\n" + " " + error_msg + "\n" + " )\n" + " return v\n" + "\n" + " def resolve_view(self, phase: str) -> ContextView:" +) + +content = content.replace(old_fields, new_fields) + +# Write the updated content +policy_file.write_text(content) + +print("Updated context_policy.py successfully") +sys.exit(0) diff --git a/src/cleveragents/domain/models/core/context_policy.py b/src/cleveragents/domain/models/core/context_policy.py index c86de8161..0b178c6e7 100644 --- a/src/cleveragents/domain/models/core/context_policy.py +++ b/src/cleveragents/domain/models/core/context_policy.py @@ -34,7 +34,7 @@ Based on ``docs/specification.md`` Context section and ADR-004. from __future__ import annotations from collections.abc import Sequence -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -47,6 +47,16 @@ if TYPE_CHECKING: VALID_PHASES: frozenset[str] = frozenset({"default", "strategize", "execute", "apply"}) +VALID_STRATEGIES: frozenset[str] = frozenset( + { + "basic", + "semantic", + "relevance_scoring", + "adaptive", + "fusion", + } +) + _INHERITANCE_CHAIN: dict[str, list[str]] = { "default": ["default"], "strategize": ["strategize", "default"], @@ -125,6 +135,9 @@ class ProjectContextPolicy(BaseModel): An empty ``ProjectContextPolicy()`` defaults to including everything (the ``default_view`` has empty include lists which means "all"). + + Optionally specifies a context assembly strategy and its + configuration parameters. """ default_view: ContextView = Field( @@ -143,6 +156,30 @@ class ProjectContextPolicy(BaseModel): default=None, description=("Overrides for Apply (inherits from execute if None)"), ) + strategy: str | None = Field( + default=None, + description=( + "Context assembly strategy name. " + "Valid values: basic, semantic, relevance_scoring, adaptive, fusion" + ), + ) + strategy_config: dict[str, Any] | None = Field( + default=None, + description="Strategy-specific configuration parameters", + ) + + @field_validator("strategy") + @classmethod + def _validate_strategy( + cls: type[ProjectContextPolicy], + v: str | None, + ) -> str | None: + """Validate that strategy name is in the list of valid strategies.""" + if v is not None and v not in VALID_STRATEGIES: + raise ValueError( + f"Invalid strategy '{v}': must be one of {sorted(VALID_STRATEGIES)}" + ) + return v def resolve_view(self, phase: str) -> ContextView: """Resolve the effective view for a given phase.