diff --git a/CHANGELOG.md b/CHANGELOG.md index 72bae9e50..7dd4b7c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -170,6 +170,13 @@ ensuring data is stored with proper parameter values. code 1 when no actor is configured. ### Added +- **ACMS Context Policy Configuration Loader and Plan Execution Integration**: + Implemented `ContextPolicyConfigurationLoader` for loading YAML/TOML policy + configurations with full schema validation, and `PlanExecutionACMSIntegration` + for wiring ACMS-assembled context into the plan execution engine. Enables + flexible, per-view context policy configuration with scope rules, priority + weights, and budget overrides. (#9584) + - **Automated CLI Docstring Example Validation** (#9106): Added `DocstringExampleValidator` in `src/cleveragents/cli/docstring_validator.py` that introspects Typer command signatures diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f266b1fb3..457dee56d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -53,6 +53,7 @@ Below are some of the specific details of various contributions. * HAL 9000 has contributed the agent-evolution-pool-supervisor PR metadata assignment (#7888): the supervisor now automatically looks up the Type/Automation label and earliest open milestone before dispatching improvement PR creation workers, ensuring all generated improvement PRs have correct Type labels and milestone assignments. * HAL 9000 has contributed the decision recording hook for the Strategize phase (issue #8522): captures every decision point with question, chosen option, alternatives, confidence, rationale, and full context snapshot for replay and correction. * HAL 9000 has contributed the ContextStrategy protocol and StrategyRegistry plugin registration system (PR #10590 / issue #8616): implemented the pluggable context assembly strategy protocol with proper type-safe method signatures, created the central thread-safe StrategyRegistry supporting registration, lookup, entry-point discovery, and per-strategy configuration (timeout, fragments limits, workers, circuit breaker threshold). Six built-in strategies implemented and documented: simple-keyword, semantic-embedding, breadth-depth-navigator, arce, temporal-archaeology, and plan-decision-context. Full BDD test coverage including thread safety, boundary validation, and error handling tests. (Part of Epic #8505) +* HAL9000 has contributed automated implementation of ACMS context policy configuration loader and plan execution integration. * This project was made possible thanks to considerable donation of time, money, and resources by CleverThis, Inc. * HAL 9000 has contributed automated bug fixes, CLI output formatting improvements, and ongoing maintenance as part of the CleverAgents automation system. * HAL 9000 has contributed the pr-review-pool-supervisor tracking prefix documentation fix (#7891): aligned all documentation references from the outdated `AUTO-REV-POOL` prefix to the correct `AUTO-REV-SUP` prefix used in production. diff --git a/benchmarks/acms_context_policy_bench.py b/benchmarks/acms_context_policy_bench.py new file mode 100644 index 000000000..82aba5d0f --- /dev/null +++ b/benchmarks/acms_context_policy_bench.py @@ -0,0 +1,178 @@ +"""ASV benchmarks for ACMS context policy loader and plan execution integration. + +Measures the performance of: +- ContextPolicyConfigurationLoader.load_from_string (YAML and TOML) +- ACMSContextAssembler.assemble_context with varying policy counts +- PlanExecutionACMSIntegration.prepare_llm_context with and without policies +- PolicyScope.matches with scalar and list values +""" + +from __future__ import annotations + +import importlib +import sys +from pathlib import Path + +_SRC = str(Path(__file__).resolve().parents[1] / "src") +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +import cleveragents # noqa: E402 + +importlib.reload(cleveragents) + +from cleveragents.acms.context_policy_loader import ( # noqa: E402 + ContextPolicyConfigurationLoader, + PolicyScope, +) +from cleveragents.acms.plan_execution_integration import ( # noqa: E402 + ACMSContextAssembler, + PlanExecutionACMSIntegration, +) + +_YAML_CONFIG_1 = """ +view_name: bench_view +policies: + - name: policy1 + priority_weight: 1.0 + scopes: + - name: file_type + value: python +""" + +_YAML_CONFIG_10 = """ +view_name: bench_view +policies: +""" + "\n".join( + f" - name: policy{i}\n priority_weight: {i}.0" + for i in range(10) +) + +_YAML_CONFIG_100 = """ +view_name: bench_view +policies: +""" + "\n".join( + f" - name: policy{i}\n priority_weight: {i}.0" + for i in range(100) +) + +_TOML_CONFIG_1 = """ +view_name = "bench_view" +[[policies]] +name = "policy1" +priority_weight = 1.0 +""" + + +class LoaderSuite: + """Benchmark ContextPolicyConfigurationLoader throughput.""" + + def setup(self) -> None: + """Set up loader instance.""" + self._loader = ContextPolicyConfigurationLoader() + + def time_load_yaml_1_policy(self) -> None: + """Benchmark loading a YAML config with 1 policy.""" + self._loader.load_from_string(_YAML_CONFIG_1, "yaml") + + def time_load_yaml_10_policies(self) -> None: + """Benchmark loading a YAML config with 10 policies.""" + self._loader.load_from_string(_YAML_CONFIG_10, "yaml") + + def time_load_yaml_100_policies(self) -> None: + """Benchmark loading a YAML config with 100 policies.""" + self._loader.load_from_string(_YAML_CONFIG_100, "yaml") + + def time_load_toml_1_policy(self) -> None: + """Benchmark loading a TOML config with 1 policy.""" + self._loader.load_from_string(_TOML_CONFIG_1, "toml") + + +class AssemblerSuite: + """Benchmark ACMSContextAssembler.assemble_context throughput.""" + + def setup(self) -> None: + """Set up assembler instances with varying policy counts.""" + loader = ContextPolicyConfigurationLoader() + + config_1 = loader.load_from_string(_YAML_CONFIG_1, "yaml") + config_10 = loader.load_from_string(_YAML_CONFIG_10, "yaml") + config_100 = loader.load_from_string(_YAML_CONFIG_100, "yaml") + + self._assembler_1 = ACMSContextAssembler(config_1) + self._assembler_10 = ACMSContextAssembler(config_10) + self._assembler_100 = ACMSContextAssembler(config_100) + + self._raw_context = { + "file_type": "python", + "path": "src/module.py", + "content": "def hello(): pass", + } + + def time_assemble_1_policy(self) -> None: + """Benchmark assembling context with 1 policy.""" + self._assembler_1.assemble_context(self._raw_context) + + def time_assemble_10_policies(self) -> None: + """Benchmark assembling context with 10 policies.""" + self._assembler_10.assemble_context(self._raw_context) + + def time_assemble_100_policies(self) -> None: + """Benchmark assembling context with 100 policies.""" + self._assembler_100.assemble_context(self._raw_context) + + +class IntegrationSuite: + """Benchmark PlanExecutionACMSIntegration.prepare_llm_context throughput.""" + + def setup(self) -> None: + """Set up integration instances.""" + loader = ContextPolicyConfigurationLoader() + config = loader.load_from_string(_YAML_CONFIG_10, "yaml") + + self._integration_no_policy = PlanExecutionACMSIntegration() + self._integration_with_policy = PlanExecutionACMSIntegration( + policy_config=config + ) + self._raw_context = { + "file_type": "python", + "path": "src/module.py", + } + + def time_prepare_context_no_policy(self) -> None: + """Benchmark prepare_llm_context with no policy (passthrough).""" + self._integration_no_policy.prepare_llm_context(self._raw_context) + + def time_prepare_context_with_policy(self) -> None: + """Benchmark prepare_llm_context with ACMS policy assembly.""" + self._integration_with_policy.prepare_llm_context(self._raw_context) + + +class PolicyScopeSuite: + """Benchmark PolicyScope.matches throughput.""" + + def setup(self) -> None: + """Set up scope instances.""" + self._scope_scalar = PolicyScope(name="file_type", value="python") + self._scope_list = PolicyScope( + name="file_type", value=["python", "javascript", "typescript"] + ) + self._context_match = {"file_type": "python"} + self._context_no_match = {"file_type": "java"} + self._context_list = {"file_type": ["python", "rust"]} + + def time_scope_scalar_match(self) -> None: + """Benchmark scalar scope match.""" + self._scope_scalar.matches(self._context_match) + + def time_scope_scalar_no_match(self) -> None: + """Benchmark scalar scope no-match.""" + self._scope_scalar.matches(self._context_no_match) + + def time_scope_list_match(self) -> None: + """Benchmark list scope match.""" + self._scope_list.matches(self._context_match) + + def time_scope_context_list_match(self) -> None: + """Benchmark scope match against list context value.""" + self._scope_scalar.matches(self._context_list) diff --git a/features/acms_context_policy_loader.feature b/features/acms_context_policy_loader.feature index 5a3dc88a3..d423838ba 100644 --- a/features/acms_context_policy_loader.feature +++ b/features/acms_context_policy_loader.feature @@ -141,7 +141,7 @@ Feature: ACMS Context Policy Configuration Loader | name | value | | file_type | python | | path | src | - When I apply the policy to context with file_type "python" and path "src" + When I apply the policy to context with multiple scopes: file_type "python" path "src" Then the policy should match the context Scenario: Policy with list values in scope diff --git a/features/steps/acms_context_policy_loader_steps.py b/features/steps/acms_context_policy_loader_steps.py index 205820fa6..72a5183af 100644 --- a/features/steps/acms_context_policy_loader_steps.py +++ b/features/steps/acms_context_policy_loader_steps.py @@ -2,57 +2,57 @@ from __future__ import annotations +import json import tempfile -from pathlib import Path -from typing import Any, Dict, List +from typing import Any -import tomllib -import yaml from behave import given, then, when from cleveragents.acms.context_policy_loader import ( - ContextPolicyConfigurationLoader, ContextPolicyConfig, + ContextPolicyConfigurationLoader, PolicyScope, ViewPolicyConfiguration, ) +from cleveragents.acms.plan_execution_integration import ( + ACMSContextAssembler, + PlanExecutionACMSIntegration, +) @given("I have a context policy configuration loader") def step_have_loader(context: Any) -> None: """Initialize a context policy configuration loader.""" context.loader = ContextPolicyConfigurationLoader() - context.config = None + context.loaded_config = None context.error = None @given("I have a YAML configuration file with:") def step_have_yaml_file(context: Any) -> None: """Create a temporary YAML configuration file.""" - context.temp_file = tempfile.NamedTemporaryFile( + with tempfile.NamedTemporaryFile( mode="w", suffix=".yaml", delete=False - ) - context.temp_file.write(context.text) - context.temp_file.close() - context.config_path = context.temp_file.name + ) as tmp: + tmp.write(context.text) + context.config_path = tmp.name @given("I have a TOML configuration file with:") def step_have_toml_file(context: Any) -> None: """Create a temporary TOML configuration file.""" - context.temp_file = tempfile.NamedTemporaryFile( + with tempfile.NamedTemporaryFile( mode="w", suffix=".toml", delete=False - ) - context.temp_file.write(context.text) - context.temp_file.close() - context.config_path = context.temp_file.name + ) as tmp: + tmp.write(context.text) + context.config_path = tmp.name @when("I load the configuration from the YAML file") def step_load_yaml_file(context: Any) -> None: """Load configuration from the YAML file.""" try: - context.config = context.loader.load(context.config_path) + context.loaded_config = context.loader.load(context.config_path) except Exception as e: context.error = e @@ -61,7 +61,7 @@ def step_load_yaml_file(context: Any) -> None: def step_load_toml_file(context: Any) -> None: """Load configuration from the TOML file.""" try: - context.config = context.loader.load(context.config_path) + context.loaded_config = context.loader.load(context.config_path) except Exception as e: context.error = e @@ -70,7 +70,7 @@ def step_load_toml_file(context: Any) -> None: def step_try_load_yaml_file(context: Any) -> None: """Try to load configuration from the YAML file.""" try: - context.config = context.loader.load(context.config_path) + context.loaded_config = context.loader.load(context.config_path) except Exception as e: context.error = e @@ -79,7 +79,7 @@ def step_try_load_yaml_file(context: Any) -> None: def step_try_load_toml_file(context: Any) -> None: """Try to load configuration from the TOML file.""" try: - context.config = context.loader.load(context.config_path) + context.loaded_config = context.loader.load(context.config_path) except Exception as e: context.error = e @@ -87,39 +87,39 @@ def step_try_load_toml_file(context: Any) -> None: @then("the configuration should have view_name {view_name}") def step_check_view_name(context: Any, view_name: str) -> None: """Check the view name in the configuration.""" - assert context.config is not None - assert context.config.view_name == view_name + assert context.loaded_config is not None + assert context.loaded_config.view_name == view_name @then("the configuration should have {count:d} policy") def step_check_policy_count(context: Any, count: int) -> None: """Check the number of policies in the configuration.""" - assert context.config is not None - assert len(context.config.policies) == count + assert context.loaded_config is not None + assert len(context.loaded_config.policies) == count @then("the first policy should have name {name}") def step_check_first_policy_name(context: Any, name: str) -> None: """Check the name of the first policy.""" - assert context.config is not None - assert len(context.config.policies) > 0 - assert context.config.policies[0].name == name + assert context.loaded_config is not None + assert len(context.loaded_config.policies) > 0 + assert context.loaded_config.policies[0].name == name @then("the first policy should have priority_weight {weight:f}") def step_check_first_policy_priority(context: Any, weight: float) -> None: """Check the priority weight of the first policy.""" - assert context.config is not None - assert len(context.config.policies) > 0 - assert context.config.policies[0].priority_weight == weight + assert context.loaded_config is not None + assert len(context.loaded_config.policies) > 0 + assert context.loaded_config.policies[0].priority_weight == weight @then("the first policy should have budget_override {budget:d}") def step_check_first_policy_budget(context: Any, budget: int) -> None: """Check the budget override of the first policy.""" - assert context.config is not None - assert len(context.config.policies) > 0 - assert context.config.policies[0].budget_override == budget + assert context.loaded_config is not None + assert len(context.loaded_config.policies) > 0 + assert context.loaded_config.policies[0].budget_override == budget @then("I should get a validation error about missing name field") @@ -154,7 +154,7 @@ def step_have_nonexistent_file(context: Any) -> None: def step_try_load_file(context: Any) -> None: """Try to load configuration from the file.""" try: - context.config = context.loader.load(context.config_path) + context.loaded_config = context.loader.load(context.config_path) except Exception as e: context.error = e @@ -166,15 +166,14 @@ def step_check_file_not_found_error(context: Any) -> None: assert isinstance(context.error, FileNotFoundError) -@given("I have a configuration file with unsupported format {format}") -def step_have_unsupported_format(context: Any, format: str) -> None: +@given("I have a configuration file with unsupported format {fmt}") +def step_have_unsupported_format(context: Any, fmt: str) -> None: """Create a file with unsupported format.""" - context.temp_file = tempfile.NamedTemporaryFile( - mode="w", suffix=format, delete=False - ) - context.temp_file.write("{}") - context.temp_file.close() - context.config_path = context.temp_file.name + with tempfile.NamedTemporaryFile( + mode="w", suffix=fmt, delete=False + ) as tmp: + tmp.write("{}") + context.config_path = tmp.name @then("I should get a ValueError about unsupported format") @@ -203,7 +202,7 @@ def step_have_toml_string(context: Any) -> None: def step_load_yaml_string(context: Any) -> None: """Load configuration from the YAML string.""" try: - context.config = context.loader.load_from_string( + context.loaded_config = context.loader.load_from_string( context.config_string, "yaml" ) except Exception as e: @@ -214,7 +213,7 @@ def step_load_yaml_string(context: Any) -> None: def step_load_toml_string(context: Any) -> None: """Load configuration from the TOML string.""" try: - context.config = context.loader.load_from_string( + context.loaded_config = context.loader.load_from_string( context.config_string, "toml" ) except Exception as e: @@ -224,7 +223,7 @@ def step_load_toml_string(context: Any) -> None: @given("I have a context policy configuration with:") def step_have_policy_config(context: Any) -> None: """Create a context policy configuration from table.""" - config_dict: Dict[str, Any] = {} + config_dict: dict[str, Any] = {} for row in context.table: key = row["view_name"] if "view_name" in row else row.get("key") @@ -260,8 +259,6 @@ def step_policy_has_scopes(context: Any) -> None: @when("I apply the policy to context with file_type {file_type}") def step_apply_policy_with_file_type(context: Any, file_type: str) -> None: """Apply policy to context with specific file type.""" - from cleveragents.acms.plan_execution_integration import ACMSContextAssembler - context.test_context = {"file_type": file_type} context.assembler = ACMSContextAssembler(context.policy_config) @@ -291,6 +288,11 @@ def step_policy1_priority(context: Any, weight: float) -> None: """Set priority weight for policy1.""" if context.policy_config.policies: context.policy_config.policies[0].priority_weight = weight + # Reinitialize integration if present (for plan execution integration tests) + if hasattr(context, "integration"): + context.integration = PlanExecutionACMSIntegration( + policy_config=context.policy_config + ) @given("policy2 has priority_weight {weight:f}") @@ -298,13 +300,16 @@ def step_policy2_priority(context: Any, weight: float) -> None: """Set priority weight for policy2.""" if len(context.policy_config.policies) > 1: context.policy_config.policies[1].priority_weight = weight + # Reinitialize integration if present (for plan execution integration tests) + if hasattr(context, "integration"): + context.integration = PlanExecutionACMSIntegration( + policy_config=context.policy_config + ) @when("I assemble context with both policies") def step_assemble_context_both(context: Any) -> None: """Assemble context with both policies.""" - from cleveragents.acms.plan_execution_integration import ACMSContextAssembler - context.assembler = ACMSContextAssembler(context.policy_config) context.assembled = context.assembler.assemble_context({}) context.applied_policies = context.assembled.get("policies_applied", []) @@ -322,13 +327,16 @@ def step_policy_budget_override(context: Any, budget: int) -> None: """Set budget override for the policy.""" if context.policy_config.policies: context.policy_config.policies[0].budget_override = budget + # Reinitialize integration if present (for plan execution integration tests) + if hasattr(context, "integration"): + context.integration = PlanExecutionACMSIntegration( + policy_config=context.policy_config + ) @when("I apply the policy to context") def step_apply_policy(context: Any) -> None: """Apply policy to context.""" - from cleveragents.acms.plan_execution_integration import ACMSContextAssembler - context.assembler = ACMSContextAssembler(context.policy_config) context.assembled = context.assembler.assemble_context({}) @@ -336,15 +344,15 @@ def step_apply_policy(context: Any) -> None: @then("the assembled context should have budget {budget:d}") def step_check_assembled_budget(context: Any, budget: int) -> None: """Check the budget in the assembled context.""" - assert context.assembled is not None - assert context.assembled["assembled_data"].get("budget") == budget + # Support both direct assembler context and integration context + assembled = getattr(context, "assembled", None) or getattr(context, "llm_context", None) + assert assembled is not None + assert assembled["assembled_data"].get("budget") == budget @when("I assemble context") def step_assemble_context(context: Any) -> None: """Assemble context.""" - from cleveragents.acms.plan_execution_integration import ACMSContextAssembler - context.assembler = ACMSContextAssembler(context.policy_config) context.assembled = context.assembler.assemble_context({}) @@ -359,8 +367,10 @@ def step_policy_disabled(context: Any) -> None: @then("the policy should not be applied") def step_policy_not_applied(context: Any) -> None: """Check that the policy was not applied.""" - assert context.assembled is not None - assert "policy1" not in context.assembled.get("policies_applied", []) + # Support both direct assembler context and integration context + assembled = getattr(context, "assembled", None) or getattr(context, "llm_context", None) + assert assembled is not None + assert "policy1" not in assembled.get("policies_applied", []) @given("the policy has metadata:") @@ -392,7 +402,7 @@ def step_policy_multiple_scopes(context: Any) -> None: context.policy_config.policies[0].scopes.append(scope) -@when("I apply the policy to context with file_type {file_type} and path {path}") +@when("I apply the policy to context with multiple scopes: file_type {file_type} path {path}") def step_apply_policy_multiple_scopes(context: Any, file_type: str, path: str) -> None: """Apply policy to context with multiple scope values.""" context.test_context = {"file_type": file_type, "path": path} @@ -404,9 +414,6 @@ def step_policy_scope_list_values(context: Any, name: str, values: str) -> None: if not context.policy_config.policies: context.policy_config.policies.append(ContextPolicyConfig(name="policy1")) - # Parse the values string (e.g., '["python", "javascript"]') - import json - value_list = json.loads(values) scope = PolicyScope(name=name, value=value_list) context.policy_config.policies[0].scopes.append(scope) diff --git a/features/steps/acms_plan_execution_integration_steps.py b/features/steps/acms_plan_execution_integration_steps.py index 483ce0365..46425d476 100644 --- a/features/steps/acms_plan_execution_integration_steps.py +++ b/features/steps/acms_plan_execution_integration_steps.py @@ -3,7 +3,7 @@ from __future__ import annotations import tempfile -from typing import Any, Dict +from typing import Any import yaml from behave import given, then, when @@ -88,12 +88,11 @@ def step_have_policy_file(context: Any) -> None: "view_name": "test_view", "policies": [{"name": "policy1"}], } - context.temp_file = tempfile.NamedTemporaryFile( + with tempfile.NamedTemporaryFile( mode="w", suffix=".yaml", delete=False - ) - yaml.dump(config_data, context.temp_file) - context.temp_file.close() - context.config_path = context.temp_file.name + ) as tmp: + yaml.dump(config_data, tmp) + context.config_path = tmp.name @when("I load the policy configuration from the file") @@ -199,20 +198,6 @@ def step_have_multiple_policies(context: Any) -> None: ) -@given("policy1 has priority_weight {weight:f}") -def step_policy1_weight(context: Any, weight: float) -> None: - """Set priority weight for policy1.""" - if context.policy_config.policies: - context.policy_config.policies[0].priority_weight = weight - - -@given("policy2 has priority_weight {weight:f}") -def step_policy2_weight(context: Any, weight: float) -> None: - """Set priority weight for policy2.""" - if len(context.policy_config.policies) > 1: - context.policy_config.policies[1].priority_weight = weight - - @when("I prepare LLM context") def step_prepare_llm_context_simple(context: Any) -> None: """Prepare LLM context.""" @@ -228,24 +213,6 @@ def step_check_policy_order(context: Any) -> None: assert policies_applied[0] == "policy2" -@given("the policy has budget_override {budget:d}") -def step_policy_budget(context: Any, budget: int) -> None: - """Set budget override for the policy.""" - if context.policy_config.policies: - context.policy_config.policies[0].budget_override = budget - context.integration = PlanExecutionACMSIntegration( - policy_config=context.policy_config - ) - - -@then("the assembled context should have budget {budget:d}") -def step_check_budget(context: Any, budget: int) -> None: - """Check that assembled context has the correct budget.""" - assert context.llm_context is not None - assembled_data = context.llm_context.get("assembled_data", {}) - assert assembled_data.get("budget") == budget - - @given("I have a policy configuration with scope rules") def step_have_scope_rules(context: Any) -> None: """Create a policy configuration with scope rules.""" @@ -289,11 +256,3 @@ def step_policy_applied(context: Any) -> None: assert context.llm_context is not None policies_applied = context.llm_context.get("policies_applied", []) assert "policy1" in policies_applied - - -@then("the policy should not be applied") -def step_policy_not_applied(context: Any) -> None: - """Check that the policy was not applied.""" - assert context.llm_context is not None - policies_applied = context.llm_context.get("policies_applied", []) - assert "policy1" not in policies_applied diff --git a/src/cleveragents/acms/context_policy_loader.py b/src/cleveragents/acms/context_policy_loader.py index 5fc8beae7..984369b40 100644 --- a/src/cleveragents/acms/context_policy_loader.py +++ b/src/cleveragents/acms/context_policy_loader.py @@ -7,20 +7,13 @@ with scope rules, priority weights, and budget overrides. from __future__ import annotations -import sys +import tomllib from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any, ClassVar import yaml -if sys.version_info >= (3, 11): - import tomllib -else: - import tomli as tomllib - -from cleveragents.domain.models.core.context_policy import ContextPolicy - @dataclass class PolicyScope: @@ -29,10 +22,10 @@ class PolicyScope: name: str """Name of the scope (e.g., 'file_type', 'path_pattern').""" - value: Union[str, List[str]] + value: str | list[str] """Value or list of values for the scope.""" - def matches(self, context: Dict[str, Any]) -> bool: + def matches(self, context: dict[str, Any]) -> bool: """Check if this scope matches the given context. Args: @@ -59,38 +52,24 @@ class ContextPolicyConfig: name: str """Name of the policy.""" - description: Optional[str] = None + description: str | None = None """Description of the policy.""" - scopes: List[PolicyScope] = field(default_factory=list) + scopes: list[PolicyScope] = field(default_factory=list) """List of scope rules for this policy.""" priority_weight: float = 1.0 """Priority weight for this policy (higher = more important).""" - budget_override: Optional[int] = None + budget_override: int | None = None """Optional budget override in tokens.""" enabled: bool = True """Whether this policy is enabled.""" - metadata: Dict[str, Any] = field(default_factory=dict) + metadata: dict[str, Any] = field(default_factory=dict) """Additional metadata for the policy.""" - def to_context_policy(self) -> ContextPolicy: - """Convert to a ContextPolicy domain model. - - Returns: - ContextPolicy instance. - """ - return ContextPolicy( - name=self.name, - description=self.description or "", - priority_weight=self.priority_weight, - budget_override=self.budget_override, - enabled=self.enabled, - ) - @dataclass class ViewPolicyConfiguration: @@ -99,29 +78,29 @@ class ViewPolicyConfiguration: view_name: str """Name of the view.""" - policies: List[ContextPolicyConfig] = field(default_factory=list) + policies: list[ContextPolicyConfig] = field(default_factory=list) """List of policies for this view.""" default_priority_weight: float = 1.0 """Default priority weight for policies in this view.""" - default_budget: Optional[int] = None + default_budget: int | None = None """Default budget for this view.""" - metadata: Dict[str, Any] = field(default_factory=dict) + metadata: dict[str, Any] = field(default_factory=dict) """Additional metadata for the view.""" class ContextPolicyConfigurationLoader: """Loader for context policy configurations from YAML/TOML files.""" - SUPPORTED_FORMATS = {"yaml", "yml", "toml"} + SUPPORTED_FORMATS: ClassVar[set[str]] = {"yaml", "yml", "toml"} def __init__(self) -> None: """Initialize the configuration loader.""" pass - def load(self, config_path: Union[str, Path]) -> ViewPolicyConfiguration: + def load(self, config_path: str | Path) -> ViewPolicyConfiguration: """Load context policy configuration from a file. Args: @@ -166,7 +145,7 @@ class ContextPolicyConfigurationLoader: Raises: ValueError: If the configuration is invalid. """ - with open(config_path, "r") as f: + with config_path.open("r") as f: data = yaml.safe_load(f) if not isinstance(data, dict): @@ -186,12 +165,12 @@ class ContextPolicyConfigurationLoader: Raises: ValueError: If the configuration is invalid. """ - with open(config_path, "rb") as f: + with config_path.open("rb") as f: data = tomllib.load(f) return self._parse_configuration(data) - def _parse_configuration(self, data: Dict[str, Any]) -> ViewPolicyConfiguration: + def _parse_configuration(self, data: dict[str, Any]) -> ViewPolicyConfiguration: """Parse configuration data into ViewPolicyConfiguration. Args: @@ -224,7 +203,7 @@ class ContextPolicyConfigurationLoader: ) def _parse_policy( - self, policy_data: Dict[str, Any], default_priority_weight: float + self, policy_data: dict[str, Any], default_priority_weight: float ) -> ContextPolicyConfig: """Parse a single policy configuration. @@ -266,7 +245,7 @@ class ContextPolicyConfigurationLoader: metadata=metadata, ) - def _parse_scope(self, scope_data: Dict[str, Any]) -> PolicyScope: + def _parse_scope(self, scope_data: dict[str, Any]) -> PolicyScope: """Parse a single scope configuration. Args: @@ -292,7 +271,7 @@ class ContextPolicyConfigurationLoader: value=scope_data["value"], ) - def _validate_schema(self, data: Dict[str, Any]) -> None: + def _validate_schema(self, data: dict[str, Any]) -> None: """Validate the configuration schema. Args: @@ -312,7 +291,7 @@ class ContextPolicyConfigurationLoader: "default_budget", "metadata", } - for field_name in data.keys(): + for field_name in data: if field_name not in allowed_fields: raise ValueError(f"Unknown field: {field_name}") @@ -344,22 +323,26 @@ class ContextPolicyConfigurationLoader: raise ValueError(f"Policy {i} scope {j} must have a 'value' field") # Validate numeric fields - if "default_priority_weight" in data: - if not isinstance(data["default_priority_weight"], (int, float)): - raise ValueError("'default_priority_weight' must be a number") + if "default_priority_weight" in data and not isinstance( + data["default_priority_weight"], (int, float) + ): + raise ValueError("'default_priority_weight' must be a number") - if "default_budget" in data: - if data["default_budget"] is not None and not isinstance( - data["default_budget"], int - ): - raise ValueError("'default_budget' must be an integer or null") + if ( + "default_budget" in data + and data["default_budget"] is not None + and not isinstance(data["default_budget"], int) + ): + raise ValueError("'default_budget' must be an integer or null") - def load_from_string(self, config_string: str, format: str = "yaml") -> ViewPolicyConfiguration: + def load_from_string( + self, config_string: str, fmt: str = "yaml" + ) -> ViewPolicyConfiguration: """Load configuration from a string. Args: config_string: Configuration string. - format: Format of the string ('yaml' or 'toml'). + fmt: Format of the string ('yaml' or 'toml'). Returns: ViewPolicyConfiguration instance. @@ -367,18 +350,18 @@ class ContextPolicyConfigurationLoader: Raises: ValueError: If the format is not supported or config is invalid. """ - if format not in self.SUPPORTED_FORMATS: + if fmt not in self.SUPPORTED_FORMATS: raise ValueError( - f"Unsupported format: {format}. " + f"Unsupported format: {fmt}. " f"Supported formats: {', '.join(self.SUPPORTED_FORMATS)}" ) - if format in ("yaml", "yml"): + if fmt in ("yaml", "yml"): data = yaml.safe_load(config_string) - elif format == "toml": + elif fmt == "toml": data = tomllib.loads(config_string) else: - raise ValueError(f"Unsupported format: {format}") + raise ValueError(f"Unsupported format: {fmt}") if not isinstance(data, dict): raise ValueError("Configuration must be a dictionary") diff --git a/src/cleveragents/acms/plan_execution_integration.py b/src/cleveragents/acms/plan_execution_integration.py index c5b2fe820..f7f868951 100644 --- a/src/cleveragents/acms/plan_execution_integration.py +++ b/src/cleveragents/acms/plan_execution_integration.py @@ -7,10 +7,11 @@ of raw file dumps. from __future__ import annotations -from typing import Any, Dict, Optional +from typing import Any from cleveragents.acms.context_policy_loader import ( ContextPolicyConfigurationLoader, + PolicyScope, ViewPolicyConfiguration, ) @@ -23,13 +24,18 @@ class ACMSContextAssembler: Args: policy_config: View policy configuration. + + Raises: + ValueError: If policy_config is None. """ + if policy_config is None: + raise ValueError("policy_config must not be None") self.policy_config = policy_config self.enabled_policies = [ p for p in policy_config.policies if p.enabled ] - def assemble_context(self, raw_context: Dict[str, Any]) -> Dict[str, Any]: + def assemble_context(self, raw_context: dict[str, Any]) -> dict[str, Any]: """Assemble context using ACMS policies. Args: @@ -38,7 +44,7 @@ class ACMSContextAssembler: Returns: ACMS-assembled context dictionary. """ - assembled_context: Dict[str, Any] = { + assembled_context: dict[str, Any] = { "view": self.policy_config.view_name, "policies_applied": [], "assembled_data": {}, @@ -61,7 +67,7 @@ class ACMSContextAssembler: return assembled_context - def _scopes_match(self, scopes: list, context: Dict[str, Any]) -> bool: + def _scopes_match(self, scopes: list[PolicyScope], context: dict[str, Any]) -> bool: """Check if all scopes match the given context. Args: @@ -77,8 +83,8 @@ class ACMSContextAssembler: return all(scope.matches(context) for scope in scopes) def _apply_policy( - self, policy: Any, raw_context: Dict[str, Any] - ) -> Dict[str, Any]: + self, policy: Any, raw_context: dict[str, Any] + ) -> dict[str, Any]: """Apply a policy to the raw context. Args: @@ -88,7 +94,7 @@ class ACMSContextAssembler: Returns: Transformed context data. """ - policy_context: Dict[str, Any] = {} + policy_context: dict[str, Any] = {} # Apply budget override if specified if policy.budget_override is not None: @@ -110,11 +116,25 @@ class ACMSContextAssembler: class PlanExecutionACMSIntegration: - """Integrates ACMS context assembly into plan execution.""" + """Integrates ACMS context assembly into plan execution. + + This class wires the ACMS context assembly pipeline into the plan + execution engine. When a policy configuration is loaded, the + ``prepare_llm_context`` method assembles context using ACMS policies + instead of passing raw file dumps to LLM calls. + + Usage with the plan execution engine:: + + integration = PlanExecutionACMSIntegration() + integration.load_policy_config("path/to/policy.yaml") + + # In RuntimeExecuteActor.execute(), replace raw context with: + llm_context = integration.prepare_llm_context(raw_context) + """ def __init__( self, - policy_config: Optional[ViewPolicyConfiguration] = None, + policy_config: ViewPolicyConfiguration | None = None, ) -> None: """Initialize the plan execution ACMS integration. @@ -122,16 +142,20 @@ class PlanExecutionACMSIntegration: policy_config: Optional view policy configuration. """ self.policy_config = policy_config - self.assembler: Optional[ACMSContextAssembler] = None + self.assembler: ACMSContextAssembler | None = None if policy_config: self.assembler = ACMSContextAssembler(policy_config) def prepare_llm_context( - self, raw_context: Dict[str, Any] - ) -> Dict[str, Any]: + self, raw_context: dict[str, Any] + ) -> dict[str, Any]: """Prepare context for LLM calls using ACMS assembly. + This method is the integration point between the plan execution + engine and the ACMS context assembly pipeline. Call this method + in place of passing raw file dumps to LLM calls. + Args: raw_context: Raw context data (e.g., file dumps). @@ -155,14 +179,14 @@ class PlanExecutionACMSIntegration: self.assembler = ACMSContextAssembler(self.policy_config) def load_policy_config_from_string( - self, config_string: str, format: str = "yaml" + self, config_string: str, fmt: str = "yaml" ) -> None: """Load policy configuration from a string. Args: config_string: Configuration string. - format: Format of the string ('yaml' or 'toml'). + fmt: Format of the string ('yaml' or 'toml'). """ loader = ContextPolicyConfigurationLoader() - self.policy_config = loader.load_from_string(config_string, format) + self.policy_config = loader.load_from_string(config_string, fmt) self.assembler = ACMSContextAssembler(self.policy_config)