feat(context): add strategy configuration to context policy YAML schema #10669
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user