feat(acms): add strategy coordinator and fusion engine
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 14s
CI / build (pull_request) Successful in 17s
CI / quality (pull_request) Successful in 18s
CI / security (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 41s
CI / unit_tests (pull_request) Successful in 2m30s
CI / integration_tests (pull_request) Successful in 3m7s
CI / docker (pull_request) Successful in 54s
CI / coverage (pull_request) Successful in 5m42s
CI / lint (push) Successful in 12s
CI / build (push) Successful in 16s
CI / quality (push) Successful in 18s
CI / security (push) Successful in 33s
CI / typecheck (push) Successful in 36s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m45s
CI / integration_tests (push) Successful in 3m11s
CI / docker (push) Successful in 46s
CI / coverage (push) Successful in 5m52s
CI / benchmark-publish (push) Successful in 17m26s
CI / benchmark-regression (pull_request) Successful in 31m25s
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 14s
CI / build (pull_request) Successful in 17s
CI / quality (pull_request) Successful in 18s
CI / security (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 41s
CI / unit_tests (pull_request) Successful in 2m30s
CI / integration_tests (pull_request) Successful in 3m7s
CI / docker (pull_request) Successful in 54s
CI / coverage (pull_request) Successful in 5m42s
CI / lint (push) Successful in 12s
CI / build (push) Successful in 16s
CI / quality (push) Successful in 18s
CI / security (push) Successful in 33s
CI / typecheck (push) Successful in 36s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m45s
CI / integration_tests (push) Successful in 3m11s
CI / docker (push) Successful in 46s
CI / coverage (push) Successful in 5m52s
CI / benchmark-publish (push) Successful in 17m26s
CI / benchmark-regression (pull_request) Successful in 31m25s
Implement StrategyCoordinator and FusionEngine as named facades over the existing ACMS pipeline components, providing clean public APIs for parallel strategy execution with proportional budget allocation and fragment fusion with dedup/conflict resolution/knapsack packing. Key changes: - Add StrategyCoordinator with parallel execution and confidence-based budget allocation - Add FusionEngine with URI+hash dedup, max-depth conflict resolution, greedy knapsack packing - Add budget overage guard with lowest-relevance fragment dropping - Add per-strategy max caps enforcement - Wire into existing ContextAssemblyPipeline - Add Behave BDD tests, Robot integration tests, ASV benchmarks - Add docs/reference/acms_fusion.md ISSUES CLOSED: #192
This commit was merged in pull request #671.
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
"""ASV benchmarks for ACMS StrategyCoordinator and FusionEngine.
|
||||
|
||||
Measures the performance of:
|
||||
- StrategyCoordinator coordination round (selection + allocation + execution)
|
||||
- StrategyCoordinator with per-strategy max caps
|
||||
- FusionEngine full fusion pipeline (dedup + depth + score + pack)
|
||||
- FusionEngine with budget overage guard
|
||||
- Integration: Coordinator → FusionEngine pipeline
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_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.application.services.acms_service import ( # noqa: E402
|
||||
StrategyCapabilities,
|
||||
)
|
||||
from cleveragents.application.services.fusion_engine import ( # noqa: E402
|
||||
FusionConfig,
|
||||
FusionEngine,
|
||||
)
|
||||
from cleveragents.application.services.strategy_coordinator import ( # noqa: E402
|
||||
CoordinatorConfig,
|
||||
StrategyCoordinator,
|
||||
)
|
||||
from cleveragents.domain.models.core.context_fragment import ( # noqa: E402
|
||||
ContextBudget,
|
||||
ContextFragment,
|
||||
FragmentProvenance,
|
||||
)
|
||||
|
||||
_DEFAULT_PROV = FragmentProvenance(resource_uri="bench://fusion")
|
||||
|
||||
|
||||
def _make_frag(**kwargs: Any) -> ContextFragment:
|
||||
kwargs.setdefault("uko_node", "bench://fusion")
|
||||
kwargs.setdefault("token_count", 10)
|
||||
kwargs.setdefault("provenance", _DEFAULT_PROV)
|
||||
return ContextFragment(**kwargs)
|
||||
|
||||
|
||||
class _BenchStrategy:
|
||||
"""Minimal strategy for benchmarks."""
|
||||
|
||||
def __init__(self, name: str = "bench", confidence: float = 0.7) -> None:
|
||||
self._name = name
|
||||
self._confidence = confidence
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def capabilities(self) -> StrategyCapabilities:
|
||||
return StrategyCapabilities()
|
||||
|
||||
def can_handle(self, request: dict[str, Any]) -> float:
|
||||
return self._confidence
|
||||
|
||||
def assemble(
|
||||
self,
|
||||
fragments: Sequence[ContextFragment],
|
||||
budget: ContextBudget,
|
||||
) -> Sequence[ContextFragment]:
|
||||
result: list[ContextFragment] = []
|
||||
total = 0
|
||||
for frag in fragments:
|
||||
if total + frag.token_count <= budget.available_tokens:
|
||||
result.append(frag)
|
||||
total += frag.token_count
|
||||
return result
|
||||
|
||||
def explain(self) -> str:
|
||||
return "Benchmark strategy."
|
||||
|
||||
|
||||
class StrategyCoordinatorSuite:
|
||||
"""Benchmark StrategyCoordinator throughput."""
|
||||
|
||||
def setup(self) -> None:
|
||||
self.coordinator = StrategyCoordinator()
|
||||
self.capped_coordinator = StrategyCoordinator(
|
||||
config=CoordinatorConfig(per_strategy_max_cap=500),
|
||||
)
|
||||
self.strategies = [
|
||||
_BenchStrategy("fast", 0.8),
|
||||
_BenchStrategy("slow", 0.4),
|
||||
]
|
||||
self.fragments = [
|
||||
_make_frag(
|
||||
uko_node=f"bench://f{i}.py",
|
||||
content=f"content_{i}",
|
||||
token_count=10,
|
||||
relevance_score=0.5,
|
||||
)
|
||||
for i in range(20)
|
||||
]
|
||||
self.budget = ContextBudget(max_tokens=500, reserved_tokens=0)
|
||||
|
||||
def time_coordinate_2_strategies(self) -> None:
|
||||
"""Coordinate 2 strategies with 20 fragments."""
|
||||
self.coordinator.coordinate(
|
||||
request={},
|
||||
strategies=self.strategies,
|
||||
budget=self.budget,
|
||||
fragments=self.fragments,
|
||||
)
|
||||
|
||||
def time_coordinate_with_caps(self) -> None:
|
||||
"""Coordinate with per-strategy max caps."""
|
||||
self.capped_coordinator.coordinate(
|
||||
request={},
|
||||
strategies=self.strategies,
|
||||
budget=self.budget,
|
||||
fragments=self.fragments,
|
||||
)
|
||||
|
||||
|
||||
class FusionEngineSuite:
|
||||
"""Benchmark FusionEngine throughput."""
|
||||
|
||||
def setup(self) -> None:
|
||||
self.engine = FusionEngine(
|
||||
config=FusionConfig(min_fragment_tokens=1),
|
||||
)
|
||||
self.budget = ContextBudget(max_tokens=300, reserved_tokens=0)
|
||||
self.fragments_small = [
|
||||
_make_frag(
|
||||
uko_node=f"bench://s{i}.py",
|
||||
content=f"small_{i}",
|
||||
token_count=10,
|
||||
relevance_score=round(0.9 - i * 0.05, 2),
|
||||
)
|
||||
for i in range(10)
|
||||
]
|
||||
self.fragments_with_dups = [
|
||||
_make_frag(
|
||||
uko_node="bench://dup.py",
|
||||
content="same content",
|
||||
token_count=10,
|
||||
relevance_score=0.9,
|
||||
),
|
||||
_make_frag(
|
||||
uko_node="bench://dup.py",
|
||||
content="same content",
|
||||
token_count=10,
|
||||
relevance_score=0.7,
|
||||
),
|
||||
*[
|
||||
_make_frag(
|
||||
uko_node=f"bench://u{i}.py",
|
||||
content=f"unique_{i}",
|
||||
token_count=10,
|
||||
relevance_score=round(0.8 - i * 0.05, 2),
|
||||
)
|
||||
for i in range(8)
|
||||
],
|
||||
]
|
||||
|
||||
def time_fuse_10_fragments(self) -> None:
|
||||
"""Fuse 10 unique fragments."""
|
||||
self.engine.fuse(self.fragments_small, self.budget)
|
||||
|
||||
def time_fuse_with_duplicates(self) -> None:
|
||||
"""Fuse fragments with duplicates requiring dedup."""
|
||||
self.engine.fuse(self.fragments_with_dups, self.budget)
|
||||
|
||||
|
||||
class IntegrationSuite:
|
||||
"""Benchmark end-to-end Coordinator → FusionEngine pipeline."""
|
||||
|
||||
def setup(self) -> None:
|
||||
self.coordinator = StrategyCoordinator()
|
||||
self.engine = FusionEngine(
|
||||
config=FusionConfig(min_fragment_tokens=1),
|
||||
)
|
||||
self.strategies = [_BenchStrategy("main", 0.8)]
|
||||
self.fragments = [
|
||||
_make_frag(
|
||||
uko_node=f"bench://i{i}.py",
|
||||
content=f"integration_{i}",
|
||||
token_count=10,
|
||||
relevance_score=round(0.9 - i * 0.02, 2),
|
||||
)
|
||||
for i in range(30)
|
||||
]
|
||||
self.budget = ContextBudget(max_tokens=200, reserved_tokens=0)
|
||||
|
||||
def time_coordinate_then_fuse(self) -> None:
|
||||
"""Full pipeline: coordinate then fuse 30 fragments."""
|
||||
coord = self.coordinator.coordinate(
|
||||
request={},
|
||||
strategies=self.strategies,
|
||||
budget=self.budget,
|
||||
fragments=self.fragments,
|
||||
)
|
||||
self.engine.fuse(coord.fragments, self.budget)
|
||||
@@ -0,0 +1,174 @@
|
||||
# ACMS Strategy Coordinator & Fusion Engine
|
||||
|
||||
## Overview
|
||||
|
||||
The **StrategyCoordinator** and **FusionEngine** are named facades that
|
||||
provide clean public APIs over the existing ACMS pipeline Phase 1 and
|
||||
Phase 2 components respectively.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ StrategyCoordinator │
|
||||
│ ┌─────────────────┐ ┌──────────────────┐ ┌──────────┐ │
|
||||
│ │ Confidence │ │ Proportional │ │ Parallel │ │
|
||||
│ │ Weighted │ │ Budget │ │ Strategy │ │
|
||||
│ │ Selector │ │ Allocator │ │ Executor │ │
|
||||
│ └────────┬────────┘ └────────┬─────────┘ └────┬─────┘ │
|
||||
│ │ select │ allocate │execute │
|
||||
│ └───────────────────┴─────────────────┘ │
|
||||
│ coordinate() │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│ fragments
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ FusionEngine │
|
||||
│ ┌──────────┐ ┌───────────┐ ┌─────────┐ ┌───────────┐ │
|
||||
│ │ Content │ │ Max Depth │ │Weighted │ │ Greedy │ │
|
||||
│ │ Hash │ │ Resolver │ │Composite│ │ Knapsack │ │
|
||||
│ │ Dedup │ │ │ │ Scorer │ │ Packer │ │
|
||||
│ └────┬─────┘ └─────┬─────┘ └────┬────┘ └─────┬─────┘ │
|
||||
│ │ dedup │ resolve │ score │ pack │
|
||||
│ └──────────────┴────────────┴─────────────┘ │
|
||||
│ fuse() │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ Budget Overage Guard │ │
|
||||
│ │ Drop lowest-relevance fragments if over budget │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## StrategyCoordinator
|
||||
|
||||
### Public API
|
||||
|
||||
```python
|
||||
coordinator = StrategyCoordinator(config=CoordinatorConfig(
|
||||
per_strategy_max_cap=500,
|
||||
min_useful_budget=64,
|
||||
))
|
||||
|
||||
result = coordinator.coordinate(
|
||||
request={"preferred_strategies": ["relevance"]},
|
||||
strategies=[strategy_a, strategy_b],
|
||||
budget=ContextBudget(max_tokens=4096),
|
||||
fragments=input_fragments,
|
||||
backends={"vector": True},
|
||||
plan_context={"plan_id": "..."},
|
||||
)
|
||||
|
||||
# Result: CoordinationResult
|
||||
# .fragments: list[ContextFragment]
|
||||
# .strategies_used: list[str]
|
||||
# .allocations: list[tuple[str, float, int]]
|
||||
# .circuit_broken: list[str]
|
||||
```
|
||||
|
||||
### Budget Allocation
|
||||
|
||||
Budget tokens are allocated **proportionally by strategy confidence**:
|
||||
|
||||
- Strategy with confidence 0.8 in a pool of (0.8 + 0.2) = 1.0
|
||||
receives 80% of the budget.
|
||||
- The largest-remainder method ensures no tokens are lost to rounding.
|
||||
- Strategies whose proportional share falls below `min_useful_budget`
|
||||
are excluded and their tokens are redistributed.
|
||||
|
||||
### Per-Strategy Max Caps
|
||||
|
||||
When `per_strategy_max_cap` is set:
|
||||
|
||||
1. Any allocation exceeding the cap is clamped.
|
||||
2. Excess tokens are redistributed proportionally among uncapped
|
||||
strategies.
|
||||
3. Redistributed tokens are also capped to prevent cascading overflows.
|
||||
|
||||
### Error Handling
|
||||
|
||||
- **Circuit breaker**: After `circuit_breaker_threshold` consecutive
|
||||
failures, a strategy is temporarily disabled. Disabled strategies
|
||||
appear in `result.circuit_broken`.
|
||||
- **Timeouts**: Per-strategy execution timeout of `executor_timeout`
|
||||
seconds. Timed-out strategies are treated as failures.
|
||||
- **Graceful degradation**: Failed strategies are excluded from results
|
||||
without failing the entire coordination round.
|
||||
|
||||
## FusionEngine
|
||||
|
||||
### Public API
|
||||
|
||||
```python
|
||||
engine = FusionEngine(config=FusionConfig(
|
||||
overage_guard_enabled=True,
|
||||
min_fragment_tokens=10,
|
||||
))
|
||||
|
||||
result = engine.fuse(
|
||||
fragments=coordinator_output.fragments,
|
||||
budget=ContextBudget(max_tokens=4096),
|
||||
)
|
||||
|
||||
# Result: FusionResult
|
||||
# .fragments: list[ContextFragment]
|
||||
# .dedup_count: int
|
||||
# .depth_resolved_count: int
|
||||
# .dropped_by_overage_guard: int
|
||||
# .total_tokens: int
|
||||
# .budget_utilization: float
|
||||
```
|
||||
|
||||
### Fragment Dedup
|
||||
|
||||
Fragments are grouped by **(UKO URI, SHA-256 hash of content)**.
|
||||
Within each group, the fragment with the highest `relevance_score`
|
||||
is retained.
|
||||
|
||||
### Detail Conflict Resolution
|
||||
|
||||
When the same UKO node appears at multiple detail depths:
|
||||
- The highest-depth rendering is retained.
|
||||
- Equal depths: prefer higher `relevance_score`.
|
||||
|
||||
### Greedy Knapsack Packing
|
||||
|
||||
Fragments are sorted with **deterministic tie-breakers**:
|
||||
|
||||
1. `relevance_score` descending
|
||||
2. `detail_depth` descending
|
||||
3. `token_count` ascending
|
||||
|
||||
The greedy algorithm fills the budget top-down. When a high-value
|
||||
fragment doesn't fit, **depth fallback** attempts lower-depth
|
||||
renderings of the same UKO node.
|
||||
|
||||
### Budget Overage Guard
|
||||
|
||||
A post-packing safety net that:
|
||||
|
||||
1. Checks if total tokens exceed the available budget.
|
||||
2. Drops fragments in ascending order of `relevance_score`.
|
||||
3. Emits structured `WARNING` log messages for each dropped fragment.
|
||||
4. Continues until total tokens fit within budget.
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### CoordinatorConfig
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `min_useful_budget` | `int` | 64 | Min tokens per strategy |
|
||||
| `executor_timeout` | `float` | 30.0 | Per-strategy timeout (seconds) |
|
||||
| `executor_max_workers` | `int` | 4 | Max concurrent threads |
|
||||
| `circuit_breaker_threshold` | `int` | 3 | Failures before circuit opens |
|
||||
| `preference_boost` | `float` | 1.5 | Confidence multiplier for preferred |
|
||||
| `per_strategy_max_cap` | `int \| None` | None | Max tokens per strategy |
|
||||
|
||||
### FusionConfig
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `scorer_weights` | `ScorerWeights \| None` | None | Composite scoring weights |
|
||||
| `depth_fallback_steps` | `tuple[int, ...]` | (9,4,2,0) | Depth fallback sequence |
|
||||
| `min_fragment_tokens` | `int` | 10 | Min tokens for packing |
|
||||
| `overage_guard_enabled` | `bool` | True | Enable budget overage guard |
|
||||
@@ -0,0 +1,168 @@
|
||||
@acms @acms_fusion
|
||||
Feature: ACMS Strategy Coordinator and Fusion Engine
|
||||
As a CleverAgents developer
|
||||
I want named facades for strategy coordination and fragment fusion
|
||||
So that the ACMS pipeline has clean public APIs for parallel strategy
|
||||
execution with proportional budget allocation and fragment fusion
|
||||
with dedup, conflict resolution, and knapsack packing
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# StrategyCoordinator — Parallel execution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@fusion @coordinator
|
||||
Scenario: StrategyCoordinator coordinates a single strategy
|
||||
Given the fusion modules are available
|
||||
And fusion test strategies with confidences 0.8 and 0.6
|
||||
And fusion test fragments with token counts 50 and 50
|
||||
When I coordinate with a budget of 200 tokens
|
||||
Then the coordination result should contain fragments
|
||||
And the coordination result should list strategies used
|
||||
|
||||
@fusion @coordinator
|
||||
Scenario: StrategyCoordinator handles empty strategies list
|
||||
Given the fusion modules are available
|
||||
And fusion test fragments with token counts 50 and 50
|
||||
When I coordinate with no strategies and a budget of 200 tokens
|
||||
Then the coordination result should contain 0 fragments
|
||||
|
||||
@fusion @coordinator
|
||||
Scenario: StrategyCoordinator reports circuit-broken strategies
|
||||
Given the fusion modules are available
|
||||
And a coordinator with a pre-broken circuit for "broken_strat"
|
||||
And fusion test fragments with token counts 50 and 50
|
||||
When I coordinate with the circuit-broken strategy
|
||||
Then the coordination result should report circuit-broken strategies
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# StrategyCoordinator — Budget allocation proportional to confidence
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@fusion @coordinator @budget
|
||||
Scenario: StrategyCoordinator allocates budget proportionally
|
||||
Given the fusion modules are available
|
||||
And fusion test strategies with confidences 0.8 and 0.2
|
||||
And fusion test fragments with token counts 50 and 50
|
||||
When I coordinate with a budget of 1000 tokens
|
||||
Then the allocation for the first strategy should be approximately 800
|
||||
And the allocation for the second strategy should be approximately 200
|
||||
And the total allocation should equal 1000
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# StrategyCoordinator — Per-strategy max caps
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@fusion @coordinator @caps
|
||||
Scenario: StrategyCoordinator enforces per-strategy max caps
|
||||
Given the fusion modules are available
|
||||
And a coordinator with per-strategy max cap of 300
|
||||
And fusion test strategies with confidences 0.9 and 0.1
|
||||
And fusion test fragments with token counts 50 and 50
|
||||
When I coordinate with a budget of 1000 tokens using the capped coordinator
|
||||
Then no strategy allocation should exceed 300 tokens
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FusionEngine — Dedup by URI + hash
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@fusion @engine @dedup
|
||||
Scenario: FusionEngine deduplicates by UKO URI and content hash
|
||||
Given the fusion modules are available
|
||||
And fusion fragments with duplicates by URI and content
|
||||
When I fuse with a budget of 500 tokens
|
||||
Then the fusion result dedup count should be greater than 0
|
||||
And the fusion result should contain fewer fragments than input
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FusionEngine — Detail conflict resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@fusion @engine @depth
|
||||
Scenario: FusionEngine resolves detail depth conflicts to max depth
|
||||
Given the fusion modules are available
|
||||
And fusion fragments with same UKO node at depths 2 and 5
|
||||
When I fuse with a budget of 500 tokens
|
||||
Then the fusion result should retain the depth 5 fragment
|
||||
And the fusion result depth resolved count should be greater than 0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FusionEngine — Greedy knapsack packing with tie-breakers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@fusion @engine @packing
|
||||
Scenario: FusionEngine packs fragments within budget using greedy knapsack
|
||||
Given the fusion modules are available
|
||||
And fusion fragments totaling 600 tokens
|
||||
When I fuse with a budget of 400 tokens
|
||||
Then the fusion result total tokens should be at most 400
|
||||
And the fusion result should contain fewer fragments than input
|
||||
|
||||
@fusion @engine @packing
|
||||
Scenario: FusionEngine uses deterministic tie-breakers
|
||||
Given the fusion modules are available
|
||||
And fusion fragments with equal relevance but different depths
|
||||
When I fuse with a budget of 500 tokens
|
||||
Then the fusion result fragments should be ordered by depth descending then token count ascending
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FusionEngine — Budget overage guard
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@fusion @engine @overage
|
||||
Scenario: FusionEngine budget overage guard drops lowest-relevance fragments
|
||||
Given the fusion modules are available
|
||||
And a fusion engine with overage guard enabled
|
||||
And fusion fragments that cause budget overage
|
||||
When I fuse with the overage-prone fragments and a budget of 100 tokens
|
||||
Then the fusion result total tokens should be at most 100
|
||||
And the fusion result dropped by overage guard should be greater than 0
|
||||
|
||||
@fusion @engine @overage
|
||||
Scenario: FusionEngine overage guard does not drop when within budget
|
||||
Given the fusion modules are available
|
||||
And a fusion engine with overage guard enabled
|
||||
And fusion fragments totaling exactly 100 tokens
|
||||
When I fuse with the exact fragments and a budget of 100 tokens
|
||||
Then the fusion result dropped by overage guard should be 0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FusionEngine — Empty input
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@fusion @engine
|
||||
Scenario: FusionEngine handles empty input gracefully
|
||||
Given the fusion modules are available
|
||||
When I fuse with no fragments and a budget of 500 tokens
|
||||
Then the fusion result should contain 0 fragments
|
||||
And the fusion result input count should be 0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Integration: Coordinator + FusionEngine
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@fusion @integration
|
||||
Scenario: StrategyCoordinator output feeds into FusionEngine
|
||||
Given the fusion modules are available
|
||||
And fusion test strategies with confidences 0.8 and 0.6
|
||||
And fusion test fragments with token counts 50 and 50
|
||||
When I coordinate then fuse with a budget of 200 tokens
|
||||
Then the final fused result should contain fragments within budget
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@fusion @config
|
||||
Scenario: CoordinatorConfig defaults are sensible
|
||||
Given the fusion modules are available
|
||||
When I create a default CoordinatorConfig
|
||||
Then the config min_useful_budget should be 64
|
||||
And the config executor_timeout should be 30.0
|
||||
And the config per_strategy_max_cap should be None
|
||||
|
||||
@fusion @config
|
||||
Scenario: FusionConfig defaults are sensible
|
||||
Given the fusion modules are available
|
||||
When I create a default FusionConfig
|
||||
Then the fusion config overage_guard_enabled should be True
|
||||
And the fusion config min_fragment_tokens should be 10
|
||||
@@ -0,0 +1,609 @@
|
||||
"""Step definitions for features/acms_fusion.feature.
|
||||
|
||||
Tests the ACMS StrategyCoordinator and FusionEngine facades, covering
|
||||
parallel strategy coordination, budget allocation, dedup, depth
|
||||
resolution, greedy knapsack packing, and budget overage guard.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
|
||||
from cleveragents.application.services.acms_pipeline import (
|
||||
CircuitBreaker,
|
||||
ParallelStrategyExecutor,
|
||||
)
|
||||
from cleveragents.application.services.acms_service import StrategyCapabilities
|
||||
from cleveragents.application.services.fusion_engine import (
|
||||
FusionConfig,
|
||||
FusionEngine,
|
||||
FusionResult,
|
||||
)
|
||||
from cleveragents.application.services.strategy_coordinator import (
|
||||
CoordinatorConfig,
|
||||
StrategyCoordinator,
|
||||
)
|
||||
from cleveragents.domain.models.core.context_fragment import (
|
||||
ContextBudget,
|
||||
ContextFragment,
|
||||
FragmentProvenance,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_DEFAULT_PROV = FragmentProvenance(resource_uri="test://fusion")
|
||||
|
||||
|
||||
def _make_frag(**kwargs: Any) -> ContextFragment:
|
||||
"""Create a ContextFragment with test defaults."""
|
||||
kwargs.setdefault("uko_node", "test://fusion/default")
|
||||
kwargs.setdefault("token_count", 10)
|
||||
kwargs.setdefault("provenance", _DEFAULT_PROV)
|
||||
return ContextFragment(**kwargs)
|
||||
|
||||
|
||||
class _FusionTestStrategy:
|
||||
"""Test strategy with configurable confidence."""
|
||||
|
||||
def __init__(self, name: str = "test_strat", confidence: float = 0.7) -> None:
|
||||
self._name = name
|
||||
self._confidence = confidence
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def capabilities(self) -> StrategyCapabilities:
|
||||
return StrategyCapabilities()
|
||||
|
||||
def can_handle(self, request: dict[str, Any]) -> float:
|
||||
return self._confidence
|
||||
|
||||
def assemble(
|
||||
self,
|
||||
fragments: Sequence[ContextFragment],
|
||||
budget: ContextBudget,
|
||||
) -> Sequence[ContextFragment]:
|
||||
result: list[ContextFragment] = []
|
||||
total = 0
|
||||
for frag in fragments:
|
||||
if total + frag.token_count <= budget.available_tokens:
|
||||
result.append(frag)
|
||||
total += frag.token_count
|
||||
return result
|
||||
|
||||
def explain(self) -> str:
|
||||
return f"Test strategy '{self._name}' with confidence {self._confidence}."
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Given steps
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("the fusion modules are available")
|
||||
def step_fusion_modules_available(context: Context) -> None:
|
||||
"""Ensure all fusion modules are importable."""
|
||||
pass
|
||||
|
||||
|
||||
@given("fusion test strategies with confidences {c1:g} and {c2:g}")
|
||||
def step_fusion_strategies(context: Context, c1: float, c2: float) -> None:
|
||||
context.fusion_strategies = [
|
||||
_FusionTestStrategy("strat_a", c1),
|
||||
_FusionTestStrategy("strat_b", c2),
|
||||
]
|
||||
|
||||
|
||||
@given("fusion test fragments with token counts {t1:d} and {t2:d}")
|
||||
def step_fusion_fragments(context: Context, t1: int, t2: int) -> None:
|
||||
context.fusion_fragments = [
|
||||
_make_frag(
|
||||
uko_node="project://fusion/a.py",
|
||||
content="alpha content",
|
||||
token_count=t1,
|
||||
relevance_score=0.9,
|
||||
),
|
||||
_make_frag(
|
||||
uko_node="project://fusion/b.py",
|
||||
content="beta content",
|
||||
token_count=t2,
|
||||
relevance_score=0.5,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@given('a coordinator with a pre-broken circuit for "{name}"')
|
||||
def step_coordinator_broken_circuit(context: Context, name: str) -> None:
|
||||
cb = CircuitBreaker(failure_threshold=1)
|
||||
cb.record_failure(name)
|
||||
executor = ParallelStrategyExecutor(circuit_breaker=cb)
|
||||
context.broken_coordinator = StrategyCoordinator(executor=executor)
|
||||
context.broken_strategy_name = name
|
||||
|
||||
|
||||
@given("a coordinator with per-strategy max cap of {cap:d}")
|
||||
def step_coordinator_with_cap(context: Context, cap: int) -> None:
|
||||
config = CoordinatorConfig(per_strategy_max_cap=cap)
|
||||
context.capped_coordinator = StrategyCoordinator(config=config)
|
||||
|
||||
|
||||
@given("fusion fragments with duplicates by URI and content")
|
||||
def step_fusion_dup_fragments(context: Context) -> None:
|
||||
context.fusion_dup_fragments = [
|
||||
_make_frag(
|
||||
uko_node="project://fusion/dup.py",
|
||||
content="same content here",
|
||||
token_count=50,
|
||||
relevance_score=0.9,
|
||||
),
|
||||
_make_frag(
|
||||
uko_node="project://fusion/dup.py",
|
||||
content="same content here",
|
||||
token_count=50,
|
||||
relevance_score=0.7,
|
||||
),
|
||||
_make_frag(
|
||||
uko_node="project://fusion/other.py",
|
||||
content="different content",
|
||||
token_count=50,
|
||||
relevance_score=0.5,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@given("fusion fragments with same UKO node at depths {d1:d} and {d2:d}")
|
||||
def step_fusion_depth_fragments(context: Context, d1: int, d2: int) -> None:
|
||||
context.fusion_depth_fragments = [
|
||||
_make_frag(
|
||||
uko_node="project://fusion/deep.py",
|
||||
content="shallow content",
|
||||
token_count=50,
|
||||
detail_depth=d1,
|
||||
relevance_score=0.8,
|
||||
),
|
||||
_make_frag(
|
||||
uko_node="project://fusion/deep.py",
|
||||
content="deep content with more detail",
|
||||
token_count=80,
|
||||
detail_depth=d2,
|
||||
relevance_score=0.7,
|
||||
),
|
||||
_make_frag(
|
||||
uko_node="project://fusion/other.py",
|
||||
content="other content",
|
||||
token_count=50,
|
||||
detail_depth=3,
|
||||
relevance_score=0.6,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@given("fusion fragments totaling {total:d} tokens")
|
||||
def step_fusion_total_fragments(context: Context, total: int) -> None:
|
||||
count = total // 100
|
||||
if count < 1:
|
||||
count = 1
|
||||
per_token = total // count
|
||||
context.fusion_total_fragments = [
|
||||
_make_frag(
|
||||
uko_node=f"project://fusion/file{i}.py",
|
||||
content=f"content_{i}",
|
||||
token_count=per_token,
|
||||
relevance_score=round(0.9 - i * 0.1, 2),
|
||||
)
|
||||
for i in range(count)
|
||||
]
|
||||
|
||||
|
||||
@given("fusion fragments with equal relevance but different depths")
|
||||
def step_fusion_tiebreaker_fragments(context: Context) -> None:
|
||||
context.fusion_tiebreaker_fragments = [
|
||||
_make_frag(
|
||||
uko_node="project://fusion/t1.py",
|
||||
content="content_t1",
|
||||
token_count=50,
|
||||
detail_depth=3,
|
||||
relevance_score=0.8,
|
||||
),
|
||||
_make_frag(
|
||||
uko_node="project://fusion/t2.py",
|
||||
content="content_t2",
|
||||
token_count=30,
|
||||
detail_depth=7,
|
||||
relevance_score=0.8,
|
||||
),
|
||||
_make_frag(
|
||||
uko_node="project://fusion/t3.py",
|
||||
content="content_t3",
|
||||
token_count=40,
|
||||
detail_depth=5,
|
||||
relevance_score=0.8,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@given("a fusion engine with overage guard enabled")
|
||||
def step_fusion_engine_overage(context: Context) -> None:
|
||||
config = FusionConfig(overage_guard_enabled=True, min_fragment_tokens=1)
|
||||
context.overage_engine = FusionEngine(config=config)
|
||||
|
||||
|
||||
@given("fusion fragments that cause budget overage")
|
||||
def step_fusion_overage_fragments(context: Context) -> None:
|
||||
# Create fragments where packer might allow overage.
|
||||
# The overage guard is a safety net for when fragments pass through
|
||||
# the packer but still exceed budget. We test this by using a custom
|
||||
# FusionEngine with a pass-through packer that doesn't enforce budget.
|
||||
context.overage_fragments = [
|
||||
_make_frag(
|
||||
uko_node=f"project://fusion/ov{i}.py",
|
||||
content=f"overage_content_{i}",
|
||||
token_count=40,
|
||||
relevance_score=round(0.9 - i * 0.2, 2),
|
||||
)
|
||||
for i in range(4)
|
||||
]
|
||||
|
||||
|
||||
@given("fusion fragments totaling exactly {total:d} tokens")
|
||||
def step_fusion_exact_fragments(context: Context, total: int) -> None:
|
||||
context.exact_fragments = [
|
||||
_make_frag(
|
||||
uko_node="project://fusion/exact.py",
|
||||
content="exact content",
|
||||
token_count=total,
|
||||
relevance_score=0.9,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# When steps — StrategyCoordinator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I coordinate with a budget of {budget:d} tokens")
|
||||
def step_coordinate(context: Context, budget: int) -> None:
|
||||
coordinator = StrategyCoordinator()
|
||||
b = ContextBudget(max_tokens=budget, reserved_tokens=0)
|
||||
context.coord_result = coordinator.coordinate(
|
||||
request={},
|
||||
strategies=context.fusion_strategies,
|
||||
budget=b,
|
||||
fragments=context.fusion_fragments,
|
||||
)
|
||||
|
||||
|
||||
@when("I coordinate with no strategies and a budget of {budget:d} tokens")
|
||||
def step_coordinate_no_strategies(context: Context, budget: int) -> None:
|
||||
coordinator = StrategyCoordinator()
|
||||
b = ContextBudget(max_tokens=budget, reserved_tokens=0)
|
||||
context.coord_result = coordinator.coordinate(
|
||||
request={},
|
||||
strategies=[],
|
||||
budget=b,
|
||||
fragments=context.fusion_fragments,
|
||||
)
|
||||
|
||||
|
||||
@when("I coordinate with the circuit-broken strategy")
|
||||
def step_coordinate_broken(context: Context) -> None:
|
||||
strategy = _FusionTestStrategy(context.broken_strategy_name, 0.7)
|
||||
b = ContextBudget(max_tokens=200, reserved_tokens=0)
|
||||
context.coord_result = context.broken_coordinator.coordinate(
|
||||
request={},
|
||||
strategies=[strategy],
|
||||
budget=b,
|
||||
fragments=context.fusion_fragments,
|
||||
)
|
||||
|
||||
|
||||
@when("I coordinate with a budget of {budget:d} tokens using the capped coordinator")
|
||||
def step_coordinate_capped(context: Context, budget: int) -> None:
|
||||
b = ContextBudget(max_tokens=budget, reserved_tokens=0)
|
||||
context.coord_result = context.capped_coordinator.coordinate(
|
||||
request={},
|
||||
strategies=context.fusion_strategies,
|
||||
budget=b,
|
||||
fragments=context.fusion_fragments,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# When steps — FusionEngine
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I fuse with a budget of {budget:d} tokens")
|
||||
def step_fuse(context: Context, budget: int) -> None:
|
||||
engine = FusionEngine(config=FusionConfig(min_fragment_tokens=1))
|
||||
b = ContextBudget(max_tokens=budget, reserved_tokens=0)
|
||||
# Use whichever fragments are available
|
||||
frags = getattr(
|
||||
context,
|
||||
"fusion_dup_fragments",
|
||||
getattr(
|
||||
context,
|
||||
"fusion_depth_fragments",
|
||||
getattr(
|
||||
context,
|
||||
"fusion_total_fragments",
|
||||
getattr(context, "fusion_tiebreaker_fragments", []),
|
||||
),
|
||||
),
|
||||
)
|
||||
context.fusion_result = engine.fuse(frags, b)
|
||||
context.fusion_input_count = len(frags)
|
||||
|
||||
|
||||
@when("I fuse with the overage-prone fragments and a budget of {budget:d} tokens")
|
||||
def step_fuse_overage(context: Context, budget: int) -> None:
|
||||
b = ContextBudget(max_tokens=budget, reserved_tokens=0)
|
||||
# Test the overage guard directly since the packer already respects budget.
|
||||
# We call _budget_overage_guard on fragments that exceed the budget.
|
||||
surviving, dropped = FusionEngine._budget_overage_guard(
|
||||
list(context.overage_fragments), b
|
||||
)
|
||||
total_tokens = sum(f.token_count for f in surviving)
|
||||
available = b.available_tokens
|
||||
context.fusion_result = FusionResult(
|
||||
fragments=surviving,
|
||||
input_count=len(context.overage_fragments),
|
||||
dedup_count=0,
|
||||
depth_resolved_count=0,
|
||||
packed_count=len(surviving),
|
||||
dropped_by_overage_guard=dropped,
|
||||
total_tokens=total_tokens,
|
||||
budget_utilization=round(
|
||||
min(total_tokens / available if available > 0 else 0.0, 1.0), 4
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@when("I fuse with the exact fragments and a budget of {budget:d} tokens")
|
||||
def step_fuse_exact(context: Context, budget: int) -> None:
|
||||
b = ContextBudget(max_tokens=budget, reserved_tokens=0)
|
||||
context.fusion_result = context.overage_engine.fuse(context.exact_fragments, b)
|
||||
|
||||
|
||||
@when("I fuse with no fragments and a budget of {budget:d} tokens")
|
||||
def step_fuse_empty(context: Context, budget: int) -> None:
|
||||
engine = FusionEngine()
|
||||
b = ContextBudget(max_tokens=budget, reserved_tokens=0)
|
||||
context.fusion_result = engine.fuse([], b)
|
||||
|
||||
|
||||
@when("I coordinate then fuse with a budget of {budget:d} tokens")
|
||||
def step_coordinate_then_fuse(context: Context, budget: int) -> None:
|
||||
coordinator = StrategyCoordinator()
|
||||
b = ContextBudget(max_tokens=budget, reserved_tokens=0)
|
||||
coord_result = coordinator.coordinate(
|
||||
request={},
|
||||
strategies=context.fusion_strategies,
|
||||
budget=b,
|
||||
fragments=context.fusion_fragments,
|
||||
)
|
||||
engine = FusionEngine(config=FusionConfig(min_fragment_tokens=1))
|
||||
context.fusion_result = engine.fuse(coord_result.fragments, b)
|
||||
context.integration_budget = budget
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# When steps — Config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I create a default CoordinatorConfig")
|
||||
def step_create_coord_config(context: Context) -> None:
|
||||
context.coord_config = CoordinatorConfig()
|
||||
|
||||
|
||||
@when("I create a default FusionConfig")
|
||||
def step_create_fusion_config(context: Context) -> None:
|
||||
context.fusion_config = FusionConfig()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Then steps — StrategyCoordinator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the coordination result should contain fragments")
|
||||
def step_coord_has_fragments(context: Context) -> None:
|
||||
assert len(context.coord_result.fragments) > 0, (
|
||||
"Expected fragments from coordination"
|
||||
)
|
||||
|
||||
|
||||
@then("the coordination result should list strategies used")
|
||||
def step_coord_strategies_used(context: Context) -> None:
|
||||
assert len(context.coord_result.strategies_used) > 0, (
|
||||
"Expected strategies used in result"
|
||||
)
|
||||
|
||||
|
||||
@then("the coordination result should contain {count:d} fragments")
|
||||
def step_coord_count(context: Context, count: int) -> None:
|
||||
assert len(context.coord_result.fragments) == count, (
|
||||
f"Expected {count} fragments, got {len(context.coord_result.fragments)}"
|
||||
)
|
||||
|
||||
|
||||
@then("the coordination result should report circuit-broken strategies")
|
||||
def step_coord_circuit_broken(context: Context) -> None:
|
||||
assert len(context.coord_result.circuit_broken) > 0, (
|
||||
"Expected circuit-broken strategies in result"
|
||||
)
|
||||
|
||||
|
||||
@then("the allocation for the first strategy should be approximately {tokens:d}")
|
||||
def step_coord_alloc_first(context: Context, tokens: int) -> None:
|
||||
first_alloc = context.coord_result.allocations[0][2]
|
||||
assert abs(first_alloc - tokens) <= 1, (
|
||||
f"First allocation {first_alloc}, expected ~{tokens}"
|
||||
)
|
||||
|
||||
|
||||
@then("the allocation for the second strategy should be approximately {tokens:d}")
|
||||
def step_coord_alloc_second(context: Context, tokens: int) -> None:
|
||||
second_alloc = context.coord_result.allocations[1][2]
|
||||
assert abs(second_alloc - tokens) <= 1, (
|
||||
f"Second allocation {second_alloc}, expected ~{tokens}"
|
||||
)
|
||||
|
||||
|
||||
@then("the total allocation should equal {total:d}")
|
||||
def step_coord_alloc_total(context: Context, total: int) -> None:
|
||||
actual = sum(a[2] for a in context.coord_result.allocations)
|
||||
assert actual == total, f"Total allocation {actual} != {total}"
|
||||
|
||||
|
||||
@then("no strategy allocation should exceed {cap:d} tokens")
|
||||
def step_coord_cap_enforced(context: Context, cap: int) -> None:
|
||||
for name, _, tokens in context.coord_result.allocations:
|
||||
assert tokens <= cap, f"Strategy {name} allocated {tokens} > cap {cap}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Then steps — FusionEngine
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the fusion result dedup count should be greater than 0")
|
||||
def step_fusion_dedup_count(context: Context) -> None:
|
||||
assert context.fusion_result.dedup_count > 0, (
|
||||
f"Expected dedup_count > 0, got {context.fusion_result.dedup_count}"
|
||||
)
|
||||
|
||||
|
||||
@then("the fusion result should contain fewer fragments than input")
|
||||
def step_fusion_fewer(context: Context) -> None:
|
||||
assert len(context.fusion_result.fragments) < context.fusion_input_count, (
|
||||
f"Expected fewer fragments ({len(context.fusion_result.fragments)}) "
|
||||
f"than input ({context.fusion_input_count})"
|
||||
)
|
||||
|
||||
|
||||
@then("the fusion result should retain the depth {depth:d} fragment")
|
||||
def step_fusion_depth_retained(context: Context, depth: int) -> None:
|
||||
depths = [f.detail_depth for f in context.fusion_result.fragments]
|
||||
# After scoring, the depth values should still be present
|
||||
# The max-depth fragment should survive for the node
|
||||
node_frags = [f for f in context.fusion_result.fragments if "deep" in f.uko_node]
|
||||
if node_frags:
|
||||
max_depth = max(f.detail_depth for f in node_frags)
|
||||
assert max_depth == depth, (
|
||||
f"Expected depth {depth} retained, got max {max_depth} in {depths}"
|
||||
)
|
||||
|
||||
|
||||
@then("the fusion result depth resolved count should be greater than 0")
|
||||
def step_fusion_depth_resolved(context: Context) -> None:
|
||||
assert context.fusion_result.depth_resolved_count > 0, (
|
||||
f"Expected depth_resolved > 0, got {context.fusion_result.depth_resolved_count}"
|
||||
)
|
||||
|
||||
|
||||
@then("the fusion result total tokens should be at most {max_tokens:d}")
|
||||
def step_fusion_within_budget(context: Context, max_tokens: int) -> None:
|
||||
assert context.fusion_result.total_tokens <= max_tokens, (
|
||||
f"Total tokens {context.fusion_result.total_tokens} > {max_tokens}"
|
||||
)
|
||||
|
||||
|
||||
@then(
|
||||
"the fusion result fragments should be ordered by depth descending "
|
||||
"then token count ascending"
|
||||
)
|
||||
def step_fusion_tiebreakers(context: Context) -> None:
|
||||
frags = context.fusion_result.fragments
|
||||
if len(frags) <= 1:
|
||||
return
|
||||
# Verify tie-breaker ordering within same relevance group
|
||||
for i in range(len(frags) - 1):
|
||||
a, b = frags[i], frags[i + 1]
|
||||
if (
|
||||
abs(a.relevance_score - b.relevance_score) < 0.001
|
||||
and a.detail_depth == b.detail_depth
|
||||
):
|
||||
# Same relevance and depth: token count ascending
|
||||
assert a.token_count <= b.token_count, (
|
||||
f"Token count tie-breaker failed: {a.token_count} > {b.token_count}"
|
||||
)
|
||||
|
||||
|
||||
@then("the fusion result dropped by overage guard should be greater than 0")
|
||||
def step_fusion_overage_dropped(context: Context) -> None:
|
||||
assert context.fusion_result.dropped_by_overage_guard > 0, (
|
||||
f"Expected drops > 0, got {context.fusion_result.dropped_by_overage_guard}"
|
||||
)
|
||||
|
||||
|
||||
@then("the fusion result dropped by overage guard should be {count:d}")
|
||||
def step_fusion_overage_exact(context: Context, count: int) -> None:
|
||||
assert context.fusion_result.dropped_by_overage_guard == count, (
|
||||
f"Expected {count} drops, got {context.fusion_result.dropped_by_overage_guard}"
|
||||
)
|
||||
|
||||
|
||||
@then("the fusion result should contain {count:d} fragments")
|
||||
def step_fusion_count(context: Context, count: int) -> None:
|
||||
assert len(context.fusion_result.fragments) == count, (
|
||||
f"Expected {count} fragments, got {len(context.fusion_result.fragments)}"
|
||||
)
|
||||
|
||||
|
||||
@then("the fusion result input count should be {count:d}")
|
||||
def step_fusion_input(context: Context, count: int) -> None:
|
||||
assert context.fusion_result.input_count == count, (
|
||||
f"Expected input_count {count}, got {context.fusion_result.input_count}"
|
||||
)
|
||||
|
||||
|
||||
@then("the final fused result should contain fragments within budget")
|
||||
def step_integration_result(context: Context) -> None:
|
||||
assert len(context.fusion_result.fragments) > 0, (
|
||||
"Expected fragments from integration"
|
||||
)
|
||||
assert context.fusion_result.total_tokens <= context.integration_budget, (
|
||||
f"Total tokens {context.fusion_result.total_tokens} "
|
||||
f"> budget {context.integration_budget}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Then steps — Config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the config min_useful_budget should be {val:d}")
|
||||
def step_config_min_budget(context: Context, val: int) -> None:
|
||||
assert context.coord_config.min_useful_budget == val
|
||||
|
||||
|
||||
@then("the config executor_timeout should be {val:g}")
|
||||
def step_config_timeout(context: Context, val: float) -> None:
|
||||
assert context.coord_config.executor_timeout == val
|
||||
|
||||
|
||||
@then("the config per_strategy_max_cap should be None")
|
||||
def step_config_cap_none(context: Context) -> None:
|
||||
assert context.coord_config.per_strategy_max_cap is None
|
||||
|
||||
|
||||
@then("the fusion config overage_guard_enabled should be True")
|
||||
def step_fusion_config_overage(context: Context) -> None:
|
||||
assert context.fusion_config.overage_guard_enabled is True
|
||||
|
||||
|
||||
@then("the fusion config min_fragment_tokens should be {val:d}")
|
||||
def step_fusion_config_min_tokens(context: Context, val: int) -> None:
|
||||
assert context.fusion_config.min_fragment_tokens == val
|
||||
@@ -0,0 +1,81 @@
|
||||
*** Settings ***
|
||||
Documentation Integration smoke tests for ACMS Strategy Coordinator and Fusion Engine
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${HELPER} ${CURDIR}/helper_acms_fusion.py
|
||||
|
||||
*** Test Cases ***
|
||||
StrategyCoordinator Coordinates Strategies
|
||||
[Documentation] StrategyCoordinator selects, allocates, and executes strategies
|
||||
${result}= Run Process ${PYTHON} ${HELPER} coord-basic cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} coord-basic-ok
|
||||
|
||||
StrategyCoordinator Budget Allocation
|
||||
[Documentation] StrategyCoordinator allocates budget proportionally to confidence
|
||||
${result}= Run Process ${PYTHON} ${HELPER} coord-budget cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} coord-budget-ok
|
||||
|
||||
StrategyCoordinator Per-Strategy Max Caps
|
||||
[Documentation] StrategyCoordinator enforces per-strategy max token caps
|
||||
${result}= Run Process ${PYTHON} ${HELPER} coord-caps cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} coord-caps-ok
|
||||
|
||||
StrategyCoordinator Circuit Breaker
|
||||
[Documentation] StrategyCoordinator reports circuit-broken strategies
|
||||
${result}= Run Process ${PYTHON} ${HELPER} coord-circuit cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} coord-circuit-ok
|
||||
|
||||
FusionEngine Dedup
|
||||
[Documentation] FusionEngine deduplicates fragments by URI + content hash
|
||||
${result}= Run Process ${PYTHON} ${HELPER} fuse-dedup cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} fuse-dedup-ok
|
||||
|
||||
FusionEngine Depth Resolution
|
||||
[Documentation] FusionEngine resolves detail depth conflicts
|
||||
${result}= Run Process ${PYTHON} ${HELPER} fuse-depth cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} fuse-depth-ok
|
||||
|
||||
FusionEngine Knapsack Packing
|
||||
[Documentation] FusionEngine packs fragments within budget using greedy knapsack
|
||||
${result}= Run Process ${PYTHON} ${HELPER} fuse-pack cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} fuse-pack-ok
|
||||
|
||||
FusionEngine Budget Overage Guard
|
||||
[Documentation] FusionEngine drops lowest-relevance fragments on budget overage
|
||||
${result}= Run Process ${PYTHON} ${HELPER} fuse-overage cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} fuse-overage-ok
|
||||
|
||||
Integration Coordinator Then Fusion
|
||||
[Documentation] StrategyCoordinator output feeds into FusionEngine
|
||||
${result}= Run Process ${PYTHON} ${HELPER} integration cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} integration-ok
|
||||
@@ -0,0 +1,352 @@
|
||||
"""Robot Framework helper for ACMS Fusion integration tests.
|
||||
|
||||
Provides a CLI-style interface for Robot to invoke StrategyCoordinator
|
||||
and FusionEngine operations and verify the results.
|
||||
|
||||
Usage:
|
||||
python robot/helper_acms_fusion.py coord-basic
|
||||
python robot/helper_acms_fusion.py coord-budget
|
||||
python robot/helper_acms_fusion.py coord-caps
|
||||
python robot/helper_acms_fusion.py coord-circuit
|
||||
python robot/helper_acms_fusion.py fuse-dedup
|
||||
python robot/helper_acms_fusion.py fuse-depth
|
||||
python robot/helper_acms_fusion.py fuse-pack
|
||||
python robot/helper_acms_fusion.py fuse-overage
|
||||
python robot/helper_acms_fusion.py integration
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Callable, Sequence
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from cleveragents.application.services.acms_pipeline import ( # noqa: E402
|
||||
CircuitBreaker,
|
||||
ParallelStrategyExecutor,
|
||||
)
|
||||
from cleveragents.application.services.acms_service import ( # noqa: E402
|
||||
StrategyCapabilities,
|
||||
)
|
||||
from cleveragents.application.services.fusion_engine import ( # noqa: E402
|
||||
FusionConfig,
|
||||
FusionEngine,
|
||||
)
|
||||
from cleveragents.application.services.strategy_coordinator import ( # noqa: E402
|
||||
CoordinatorConfig,
|
||||
StrategyCoordinator,
|
||||
)
|
||||
from cleveragents.domain.models.core.context_fragment import ( # noqa: E402
|
||||
ContextBudget,
|
||||
ContextFragment,
|
||||
FragmentProvenance,
|
||||
)
|
||||
|
||||
_DEFAULT_PROV = FragmentProvenance(resource_uri="test://robot-fusion")
|
||||
|
||||
|
||||
def _make_frag(**kwargs: Any) -> ContextFragment:
|
||||
kwargs.setdefault("uko_node", "test://robot-fusion")
|
||||
kwargs.setdefault("token_count", 10)
|
||||
kwargs.setdefault("provenance", _DEFAULT_PROV)
|
||||
return ContextFragment(**kwargs)
|
||||
|
||||
|
||||
class _TestStrategy:
|
||||
"""Minimal strategy for robot tests."""
|
||||
|
||||
def __init__(self, name: str = "robot_strat", confidence: float = 0.7) -> None:
|
||||
self._name = name
|
||||
self._confidence = confidence
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def capabilities(self) -> StrategyCapabilities:
|
||||
return StrategyCapabilities()
|
||||
|
||||
def can_handle(self, request: dict[str, Any]) -> float:
|
||||
return self._confidence
|
||||
|
||||
def assemble(
|
||||
self,
|
||||
fragments: Sequence[ContextFragment],
|
||||
budget: ContextBudget,
|
||||
) -> Sequence[ContextFragment]:
|
||||
result: list[ContextFragment] = []
|
||||
total = 0
|
||||
for frag in fragments:
|
||||
if total + frag.token_count <= budget.available_tokens:
|
||||
result.append(frag)
|
||||
total += frag.token_count
|
||||
return result
|
||||
|
||||
def explain(self) -> str:
|
||||
return f"Robot test strategy '{self._name}'."
|
||||
|
||||
|
||||
def _cmd_coord_basic() -> int:
|
||||
"""StrategyCoordinator basic coordination."""
|
||||
coordinator = StrategyCoordinator()
|
||||
strategies = [_TestStrategy("a", 0.8), _TestStrategy("b", 0.6)]
|
||||
frags = [
|
||||
_make_frag(
|
||||
uko_node="p://a.py", content="alpha", token_count=50, relevance_score=0.9
|
||||
),
|
||||
_make_frag(
|
||||
uko_node="p://b.py", content="beta", token_count=50, relevance_score=0.5
|
||||
),
|
||||
]
|
||||
b = ContextBudget(max_tokens=200, reserved_tokens=0)
|
||||
result = coordinator.coordinate(
|
||||
request={},
|
||||
strategies=strategies,
|
||||
budget=b,
|
||||
fragments=frags,
|
||||
)
|
||||
assert len(result.fragments) > 0, "Expected fragments"
|
||||
assert len(result.strategies_used) > 0, "Expected strategies used"
|
||||
print("coord-basic-ok")
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_coord_budget() -> int:
|
||||
"""StrategyCoordinator proportional budget allocation."""
|
||||
coordinator = StrategyCoordinator()
|
||||
strategies = [_TestStrategy("a", 0.8), _TestStrategy("b", 0.2)]
|
||||
frags = [
|
||||
_make_frag(
|
||||
uko_node="p://a.py", content="alpha", token_count=50, relevance_score=0.9
|
||||
),
|
||||
]
|
||||
b = ContextBudget(max_tokens=1000, reserved_tokens=0)
|
||||
result = coordinator.coordinate(
|
||||
request={},
|
||||
strategies=strategies,
|
||||
budget=b,
|
||||
fragments=frags,
|
||||
)
|
||||
total_alloc = sum(a[2] for a in result.allocations)
|
||||
assert total_alloc == 1000, f"Total {total_alloc} != 1000"
|
||||
first_alloc = result.allocations[0][2]
|
||||
assert abs(first_alloc - 800) <= 1, f"First {first_alloc} != ~800"
|
||||
print("coord-budget-ok")
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_coord_caps() -> int:
|
||||
"""StrategyCoordinator per-strategy max caps."""
|
||||
config = CoordinatorConfig(per_strategy_max_cap=300)
|
||||
coordinator = StrategyCoordinator(config=config)
|
||||
strategies = [_TestStrategy("a", 0.9), _TestStrategy("b", 0.1)]
|
||||
frags = [
|
||||
_make_frag(
|
||||
uko_node="p://a.py", content="alpha", token_count=50, relevance_score=0.9
|
||||
),
|
||||
]
|
||||
b = ContextBudget(max_tokens=1000, reserved_tokens=0)
|
||||
result = coordinator.coordinate(
|
||||
request={},
|
||||
strategies=strategies,
|
||||
budget=b,
|
||||
fragments=frags,
|
||||
)
|
||||
for name, _, tokens in result.allocations:
|
||||
assert tokens <= 300, f"Strategy {name} got {tokens} > 300"
|
||||
print("coord-caps-ok")
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_coord_circuit() -> int:
|
||||
"""StrategyCoordinator circuit breaker reporting."""
|
||||
cb = CircuitBreaker(failure_threshold=1)
|
||||
cb.record_failure("broken")
|
||||
executor = ParallelStrategyExecutor(circuit_breaker=cb)
|
||||
coordinator = StrategyCoordinator(executor=executor)
|
||||
strategies = [_TestStrategy("broken", 0.7)]
|
||||
frags = [
|
||||
_make_frag(
|
||||
uko_node="p://a.py", content="alpha", token_count=50, relevance_score=0.9
|
||||
),
|
||||
]
|
||||
b = ContextBudget(max_tokens=200, reserved_tokens=0)
|
||||
result = coordinator.coordinate(
|
||||
request={},
|
||||
strategies=strategies,
|
||||
budget=b,
|
||||
fragments=frags,
|
||||
)
|
||||
assert "broken" in result.circuit_broken, "Expected 'broken' in circuit_broken"
|
||||
print("coord-circuit-ok")
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_fuse_dedup() -> int:
|
||||
"""FusionEngine deduplication."""
|
||||
engine = FusionEngine(config=FusionConfig(min_fragment_tokens=1))
|
||||
frags = [
|
||||
_make_frag(
|
||||
uko_node="p://dup.py", content="same", token_count=50, relevance_score=0.9
|
||||
),
|
||||
_make_frag(
|
||||
uko_node="p://dup.py", content="same", token_count=50, relevance_score=0.7
|
||||
),
|
||||
_make_frag(
|
||||
uko_node="p://other.py",
|
||||
content="other",
|
||||
token_count=50,
|
||||
relevance_score=0.5,
|
||||
),
|
||||
]
|
||||
b = ContextBudget(max_tokens=500, reserved_tokens=0)
|
||||
result = engine.fuse(frags, b)
|
||||
assert result.dedup_count > 0, f"dedup_count={result.dedup_count}"
|
||||
assert len(result.fragments) < len(frags), "Expected fewer fragments"
|
||||
print("fuse-dedup-ok")
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_fuse_depth() -> int:
|
||||
"""FusionEngine depth resolution."""
|
||||
engine = FusionEngine(config=FusionConfig(min_fragment_tokens=1))
|
||||
frags = [
|
||||
_make_frag(
|
||||
uko_node="p://deep.py",
|
||||
content="shallow",
|
||||
token_count=50,
|
||||
detail_depth=2,
|
||||
relevance_score=0.8,
|
||||
),
|
||||
_make_frag(
|
||||
uko_node="p://deep.py",
|
||||
content="deep detail",
|
||||
token_count=80,
|
||||
detail_depth=5,
|
||||
relevance_score=0.7,
|
||||
),
|
||||
_make_frag(
|
||||
uko_node="p://other.py",
|
||||
content="other",
|
||||
token_count=50,
|
||||
detail_depth=3,
|
||||
relevance_score=0.6,
|
||||
),
|
||||
]
|
||||
b = ContextBudget(max_tokens=500, reserved_tokens=0)
|
||||
result = engine.fuse(frags, b)
|
||||
assert result.depth_resolved_count > 0, (
|
||||
f"depth_resolved={result.depth_resolved_count}"
|
||||
)
|
||||
deep_frags = [f for f in result.fragments if "deep" in f.uko_node]
|
||||
if deep_frags:
|
||||
assert max(f.detail_depth for f in deep_frags) == 5
|
||||
print("fuse-depth-ok")
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_fuse_pack() -> int:
|
||||
"""FusionEngine knapsack packing."""
|
||||
engine = FusionEngine(config=FusionConfig(min_fragment_tokens=1))
|
||||
frags = [
|
||||
_make_frag(
|
||||
uko_node=f"p://f{i}.py",
|
||||
content=f"c{i}",
|
||||
token_count=100,
|
||||
relevance_score=round(0.9 - i * 0.1, 2),
|
||||
)
|
||||
for i in range(6)
|
||||
]
|
||||
b = ContextBudget(max_tokens=400, reserved_tokens=0)
|
||||
result = engine.fuse(frags, b)
|
||||
assert result.total_tokens <= 400, f"total_tokens={result.total_tokens}"
|
||||
print("fuse-pack-ok")
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_fuse_overage() -> int:
|
||||
"""FusionEngine budget overage guard."""
|
||||
config = FusionConfig(overage_guard_enabled=True, min_fragment_tokens=1)
|
||||
engine = FusionEngine(config=config)
|
||||
frags = [
|
||||
_make_frag(
|
||||
uko_node=f"p://ov{i}.py",
|
||||
content=f"ov{i}",
|
||||
token_count=40,
|
||||
relevance_score=round(0.9 - i * 0.2, 2),
|
||||
)
|
||||
for i in range(4)
|
||||
]
|
||||
b = ContextBudget(max_tokens=100, reserved_tokens=0)
|
||||
result = engine.fuse(frags, b)
|
||||
assert result.total_tokens <= 100, f"total_tokens={result.total_tokens}"
|
||||
print("fuse-overage-ok")
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_integration() -> int:
|
||||
"""Integration: coordinator -> fusion."""
|
||||
coordinator = StrategyCoordinator()
|
||||
strategies = [_TestStrategy("a", 0.8)]
|
||||
frags = [
|
||||
_make_frag(
|
||||
uko_node="p://a.py", content="alpha", token_count=50, relevance_score=0.9
|
||||
),
|
||||
_make_frag(
|
||||
uko_node="p://b.py", content="beta", token_count=50, relevance_score=0.5
|
||||
),
|
||||
]
|
||||
b = ContextBudget(max_tokens=200, reserved_tokens=0)
|
||||
coord_result = coordinator.coordinate(
|
||||
request={},
|
||||
strategies=strategies,
|
||||
budget=b,
|
||||
fragments=frags,
|
||||
)
|
||||
engine = FusionEngine(config=FusionConfig(min_fragment_tokens=1))
|
||||
fuse_result = engine.fuse(coord_result.fragments, b)
|
||||
assert fuse_result.total_tokens <= 200
|
||||
assert len(fuse_result.fragments) > 0
|
||||
print("integration-ok")
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI dispatcher
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_COMMANDS: dict[str, Callable[[], int]] = {
|
||||
"coord-basic": _cmd_coord_basic,
|
||||
"coord-budget": _cmd_coord_budget,
|
||||
"coord-caps": _cmd_coord_caps,
|
||||
"coord-circuit": _cmd_coord_circuit,
|
||||
"fuse-dedup": _cmd_fuse_dedup,
|
||||
"fuse-depth": _cmd_fuse_depth,
|
||||
"fuse-pack": _cmd_fuse_pack,
|
||||
"fuse-overage": _cmd_fuse_overage,
|
||||
"integration": _cmd_integration,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
|
||||
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
return _COMMANDS[sys.argv[1]]()
|
||||
except Exception as exc:
|
||||
print(f"FAIL: {exc}", file=sys.stderr)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -71,6 +71,11 @@ from cleveragents.application.services.execution_environment_resolver import (
|
||||
ContainerUnavailableError,
|
||||
ExecutionEnvironmentResolver,
|
||||
)
|
||||
from cleveragents.application.services.fusion_engine import (
|
||||
FusionConfig,
|
||||
FusionEngine,
|
||||
FusionResult,
|
||||
)
|
||||
from cleveragents.application.services.invariant_service import (
|
||||
InvariantService,
|
||||
)
|
||||
@@ -128,6 +133,11 @@ from cleveragents.application.services.skeleton_compressor import (
|
||||
from cleveragents.application.services.skill_registry_service import (
|
||||
SkillRegistryService,
|
||||
)
|
||||
from cleveragents.application.services.strategy_coordinator import (
|
||||
CoordinationResult,
|
||||
CoordinatorConfig,
|
||||
StrategyCoordinator,
|
||||
)
|
||||
from cleveragents.application.services.strategy_registry import (
|
||||
StrategyNotFoundError,
|
||||
StrategyRegistrationError,
|
||||
@@ -198,6 +208,8 @@ __all__ = [
|
||||
"ContainerUnavailableError",
|
||||
"ContentHashDeduplicator",
|
||||
"ContextFragment",
|
||||
"CoordinationResult",
|
||||
"CoordinatorConfig",
|
||||
"CorrectionService",
|
||||
"CrossPlanCorrectionService",
|
||||
"DecisionNotFoundError",
|
||||
@@ -216,6 +228,9 @@ __all__ = [
|
||||
"DuplicateImportRule",
|
||||
"ExecutionEnvironmentResolver",
|
||||
"FileMergeOutcome",
|
||||
"FusionConfig",
|
||||
"FusionEngine",
|
||||
"FusionResult",
|
||||
"GreedyKnapsackPacker",
|
||||
"InvariantService",
|
||||
"MaxDepthResolver",
|
||||
@@ -256,6 +271,7 @@ __all__ = [
|
||||
"SpawnResult",
|
||||
"SpawnValidationError",
|
||||
"SpawnValidationResult",
|
||||
"StrategyCoordinator",
|
||||
"StrategyNotFoundError",
|
||||
"StrategyRegistrationError",
|
||||
"StrategyRegistry",
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
"""ACMS Fusion Engine — facade over Phase 2 pipeline components.
|
||||
|
||||
Provides ``FusionEngine``, a high-level facade that wraps the four
|
||||
Phase 2 pipeline components (deduplication, depth resolution, scoring,
|
||||
and budget packing) behind a single ``fuse`` call.
|
||||
|
||||
Key responsibilities:
|
||||
|
||||
- **Fragment dedup**: Delegates to ``ContentHashDeduplicator`` to remove
|
||||
duplicate fragments by UKO URI + content hash.
|
||||
- **Detail conflict resolution**: Delegates to ``MaxDepthResolver`` to
|
||||
retain the highest-depth rendering when the same UKO node appears at
|
||||
multiple depths.
|
||||
- **Composite scoring**: Delegates to ``WeightedCompositeScorer`` to
|
||||
compute weighted relevance scores across multiple factors.
|
||||
- **Greedy knapsack packing**: Delegates to ``GreedyKnapsackPacker``
|
||||
with deterministic tie-breakers (relevance, detail level, token count).
|
||||
- **Budget overage guard**: Post-packing validation that drops
|
||||
lowest-relevance fragments when the total exceeds the budget,
|
||||
emitting structured warnings.
|
||||
|
||||
Based on ``docs/specification.md`` §42930-42937 and issue #192.
|
||||
|
||||
ISSUES CLOSED: #192
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from cleveragents.application.services.acms_phase2 import (
|
||||
ContentHashDeduplicator,
|
||||
GreedyKnapsackPacker,
|
||||
MaxDepthResolver,
|
||||
ScorerWeights,
|
||||
WeightedCompositeScorer,
|
||||
)
|
||||
from cleveragents.domain.models.core.context_fragment import (
|
||||
ContextBudget,
|
||||
ContextFragment,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FusionConfig:
|
||||
"""Configuration for :class:`FusionEngine`.
|
||||
|
||||
Attributes:
|
||||
scorer_weights: Weights for the composite scorer. When
|
||||
``None``, :class:`ScorerWeights` defaults are used.
|
||||
depth_fallback_steps: Depth fallback sequence for the
|
||||
greedy knapsack packer.
|
||||
min_fragment_tokens: Minimum token threshold for packing.
|
||||
overage_guard_enabled: Whether to run the budget overage
|
||||
guard after packing.
|
||||
"""
|
||||
|
||||
scorer_weights: ScorerWeights | None = None
|
||||
depth_fallback_steps: tuple[int, ...] = (9, 4, 2, 0)
|
||||
min_fragment_tokens: int = 10
|
||||
overage_guard_enabled: bool = True
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FusionResult:
|
||||
"""Result of a :meth:`FusionEngine.fuse` call.
|
||||
|
||||
Attributes:
|
||||
fragments: Fused fragments fitting within the budget.
|
||||
input_count: Number of fragments received.
|
||||
dedup_count: Number of duplicates removed.
|
||||
depth_resolved_count: Number of depth conflicts resolved.
|
||||
packed_count: Number of fragments after packing.
|
||||
dropped_by_overage_guard: Number of fragments dropped by the
|
||||
budget overage guard.
|
||||
total_tokens: Total token count of the final fragments.
|
||||
budget_utilization: Fraction of budget used (0.0-1.0).
|
||||
"""
|
||||
|
||||
fragments: list[ContextFragment] = field(default_factory=list)
|
||||
input_count: int = 0
|
||||
dedup_count: int = 0
|
||||
depth_resolved_count: int = 0
|
||||
packed_count: int = 0
|
||||
dropped_by_overage_guard: int = 0
|
||||
total_tokens: int = 0
|
||||
budget_utilization: float = 0.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FusionEngine
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class FusionEngine:
|
||||
"""High-level facade over ACMS Phase 2 pipeline components.
|
||||
|
||||
Wraps ``ContentHashDeduplicator``, ``MaxDepthResolver``,
|
||||
``WeightedCompositeScorer``, and ``GreedyKnapsackPacker`` behind a
|
||||
single ``fuse`` method that handles the full fragment fusion pipeline.
|
||||
|
||||
Example::
|
||||
|
||||
engine = FusionEngine()
|
||||
result = engine.fuse(
|
||||
fragments=[frag1, frag2, frag3],
|
||||
budget=ContextBudget(max_tokens=2048),
|
||||
)
|
||||
for frag in result.fragments:
|
||||
print(frag.content)
|
||||
|
||||
Based on ``docs/specification.md`` §42930-42937 and issue #192.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: FusionConfig | None = None,
|
||||
*,
|
||||
deduplicator: ContentHashDeduplicator | None = None,
|
||||
depth_resolver: MaxDepthResolver | None = None,
|
||||
scorer: WeightedCompositeScorer | None = None,
|
||||
packer: GreedyKnapsackPacker | None = None,
|
||||
) -> None:
|
||||
self._config = config or FusionConfig()
|
||||
self._deduplicator = deduplicator or ContentHashDeduplicator()
|
||||
self._depth_resolver = depth_resolver or MaxDepthResolver()
|
||||
self._scorer = scorer or WeightedCompositeScorer(
|
||||
weights=self._config.scorer_weights,
|
||||
)
|
||||
self._packer = packer or GreedyKnapsackPacker(
|
||||
depth_fallback_steps=self._config.depth_fallback_steps,
|
||||
min_fragment_tokens=self._config.min_fragment_tokens,
|
||||
)
|
||||
|
||||
@property
|
||||
def config(self) -> FusionConfig:
|
||||
"""Return the engine configuration."""
|
||||
return self._config
|
||||
|
||||
@property
|
||||
def deduplicator(self) -> ContentHashDeduplicator:
|
||||
"""Return the underlying deduplicator."""
|
||||
return self._deduplicator
|
||||
|
||||
@property
|
||||
def depth_resolver(self) -> MaxDepthResolver:
|
||||
"""Return the underlying depth resolver."""
|
||||
return self._depth_resolver
|
||||
|
||||
@property
|
||||
def scorer(self) -> WeightedCompositeScorer:
|
||||
"""Return the underlying scorer."""
|
||||
return self._scorer
|
||||
|
||||
@property
|
||||
def packer(self) -> GreedyKnapsackPacker:
|
||||
"""Return the underlying packer."""
|
||||
return self._packer
|
||||
|
||||
def fuse(
|
||||
self,
|
||||
fragments: Sequence[ContextFragment],
|
||||
budget: ContextBudget,
|
||||
) -> FusionResult:
|
||||
"""Fuse fragments through dedup, depth resolution, scoring, and packing.
|
||||
|
||||
Args:
|
||||
fragments: Input fragments from strategy execution.
|
||||
budget: Token budget constraint.
|
||||
|
||||
Returns:
|
||||
A :class:`FusionResult` with fused fragments and metrics.
|
||||
"""
|
||||
input_count = len(fragments)
|
||||
|
||||
if not fragments:
|
||||
return FusionResult(input_count=0)
|
||||
|
||||
# Stage 1: Deduplicate by UKO URI + content hash
|
||||
deduped = list(self._deduplicator.deduplicate(fragments))
|
||||
dedup_count = input_count - len(deduped)
|
||||
|
||||
# Stage 2: Resolve detail depth conflicts (max depth wins)
|
||||
resolved = list(self._depth_resolver.resolve(deduped))
|
||||
depth_resolved_count = len(deduped) - len(resolved)
|
||||
|
||||
# Stage 3: Score fragments with weighted composite
|
||||
scored = list(self._scorer.score(resolved))
|
||||
|
||||
# Stage 4: Apply deterministic sort before packing
|
||||
# Tie-breakers: relevance desc, detail_depth desc, token_count asc
|
||||
sorted_scored = sorted(
|
||||
scored,
|
||||
key=lambda f: (-f.relevance_score, -f.detail_depth, f.token_count),
|
||||
)
|
||||
|
||||
# Stage 5: Greedy knapsack packing
|
||||
packed = list(self._packer.pack(sorted_scored, budget))
|
||||
packed_count = len(packed)
|
||||
|
||||
# Stage 6: Budget overage guard
|
||||
dropped_count = 0
|
||||
if self._config.overage_guard_enabled:
|
||||
packed, dropped_count = self._budget_overage_guard(packed, budget)
|
||||
|
||||
total_tokens = sum(f.token_count for f in packed)
|
||||
available = budget.available_tokens
|
||||
utilization = total_tokens / available if available > 0 else 0.0
|
||||
|
||||
logger.info(
|
||||
"Fusion complete",
|
||||
extra={
|
||||
"input_count": input_count,
|
||||
"dedup_count": dedup_count,
|
||||
"depth_resolved": depth_resolved_count,
|
||||
"packed_count": packed_count,
|
||||
"dropped_by_guard": dropped_count,
|
||||
"total_tokens": total_tokens,
|
||||
"budget_utilization": round(utilization, 4),
|
||||
},
|
||||
)
|
||||
|
||||
return FusionResult(
|
||||
fragments=packed,
|
||||
input_count=input_count,
|
||||
dedup_count=dedup_count,
|
||||
depth_resolved_count=depth_resolved_count,
|
||||
packed_count=len(packed),
|
||||
dropped_by_overage_guard=dropped_count,
|
||||
total_tokens=total_tokens,
|
||||
budget_utilization=round(min(utilization, 1.0), 4),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _budget_overage_guard(
|
||||
fragments: list[ContextFragment],
|
||||
budget: ContextBudget,
|
||||
) -> tuple[list[ContextFragment], int]:
|
||||
"""Drop lowest-relevance fragments until total fits within budget.
|
||||
|
||||
Returns:
|
||||
Tuple of (surviving fragments, number dropped).
|
||||
"""
|
||||
total = sum(f.token_count for f in fragments)
|
||||
available = budget.available_tokens
|
||||
|
||||
if total <= available:
|
||||
return fragments, 0
|
||||
|
||||
# Sort ascending by relevance so we drop least relevant first
|
||||
by_relevance = sorted(
|
||||
enumerate(fragments),
|
||||
key=lambda pair: (
|
||||
pair[1].relevance_score,
|
||||
pair[1].detail_depth,
|
||||
-pair[1].token_count,
|
||||
),
|
||||
)
|
||||
|
||||
dropped_indices: set[int] = set()
|
||||
|
||||
for idx, frag in by_relevance:
|
||||
if total <= available:
|
||||
break
|
||||
total -= frag.token_count
|
||||
dropped_indices.add(idx)
|
||||
logger.warning(
|
||||
"Budget overage guard: dropping fragment",
|
||||
extra={
|
||||
"fragment_id": frag.fragment_id,
|
||||
"uko_node": frag.uko_node,
|
||||
"relevance_score": frag.relevance_score,
|
||||
"token_count": frag.token_count,
|
||||
},
|
||||
)
|
||||
|
||||
surviving = [f for i, f in enumerate(fragments) if i not in dropped_indices]
|
||||
return surviving, len(dropped_indices)
|
||||
@@ -0,0 +1,338 @@
|
||||
"""ACMS Strategy Coordinator — facade over Phase 1 pipeline components.
|
||||
|
||||
Provides ``StrategyCoordinator``, a high-level facade that wraps the
|
||||
three Phase 1 pipeline components (strategy selection, budget allocation,
|
||||
and parallel execution) behind a single ``coordinate`` call.
|
||||
|
||||
Key responsibilities:
|
||||
|
||||
- **Strategy selection**: Delegates to ``ConfidenceWeightedSelector`` to
|
||||
rank strategies by confidence with optional preference boosting.
|
||||
- **Budget allocation**: Delegates to ``ProportionalBudgetAllocator``
|
||||
with per-strategy max cap enforcement.
|
||||
- **Parallel execution**: Delegates to ``ParallelStrategyExecutor`` with
|
||||
circuit-breaker fault tolerance and configurable timeouts.
|
||||
- **Error handling**: Graceful degradation when strategies fail —
|
||||
circuit-broken or timed-out strategies are excluded from results.
|
||||
|
||||
Based on ``docs/specification.md`` ~line 42615 and issue #192.
|
||||
|
||||
ISSUES CLOSED: #192
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from cleveragents.application.services.acms_pipeline import (
|
||||
CircuitBreaker,
|
||||
ConfidenceWeightedSelector,
|
||||
ParallelStrategyExecutor,
|
||||
ProportionalBudgetAllocator,
|
||||
)
|
||||
from cleveragents.application.services.acms_service import ContextStrategy
|
||||
from cleveragents.domain.models.core.context_fragment import (
|
||||
ContextBudget,
|
||||
ContextFragment,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CoordinatorConfig:
|
||||
"""Configuration for :class:`StrategyCoordinator`.
|
||||
|
||||
Attributes:
|
||||
min_useful_budget: Minimum token allocation below which a
|
||||
strategy is excluded from execution.
|
||||
executor_timeout: Per-strategy timeout in seconds.
|
||||
executor_max_workers: Maximum concurrent strategy threads.
|
||||
circuit_breaker_threshold: Consecutive failures before a
|
||||
strategy's circuit is opened.
|
||||
preference_boost: Confidence multiplier for preferred strategies.
|
||||
per_strategy_max_cap: Optional per-strategy token cap. When set,
|
||||
no strategy receives more than this many tokens regardless
|
||||
of its proportional share.
|
||||
"""
|
||||
|
||||
min_useful_budget: int = 64
|
||||
executor_timeout: float = 30.0
|
||||
executor_max_workers: int = 4
|
||||
circuit_breaker_threshold: int = 3
|
||||
preference_boost: float = 1.5
|
||||
per_strategy_max_cap: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CoordinationResult:
|
||||
"""Result of a :meth:`StrategyCoordinator.coordinate` call.
|
||||
|
||||
Attributes:
|
||||
fragments: Collected fragments from all executed strategies.
|
||||
strategies_used: Names of strategies that contributed fragments.
|
||||
allocations: Budget allocations as (name, confidence, tokens)
|
||||
triples.
|
||||
circuit_broken: Strategy names that were circuit-broken and
|
||||
therefore skipped.
|
||||
"""
|
||||
|
||||
fragments: list[ContextFragment] = field(default_factory=list)
|
||||
strategies_used: list[str] = field(default_factory=list)
|
||||
allocations: list[tuple[str, float, int]] = field(default_factory=list)
|
||||
circuit_broken: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# StrategyCoordinator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class StrategyCoordinator:
|
||||
"""High-level facade over ACMS Phase 1 pipeline components.
|
||||
|
||||
Wraps ``ConfidenceWeightedSelector``, ``ProportionalBudgetAllocator``,
|
||||
and ``ParallelStrategyExecutor`` behind a single ``coordinate`` method
|
||||
that handles the full strategy orchestration lifecycle.
|
||||
|
||||
Example::
|
||||
|
||||
coordinator = StrategyCoordinator()
|
||||
result = coordinator.coordinate(
|
||||
request={"preferred_strategies": ["relevance"]},
|
||||
strategies=[RelevanceStrategy(), RecencyStrategy()],
|
||||
budget=ContextBudget(max_tokens=4096),
|
||||
)
|
||||
for frag in result.fragments:
|
||||
print(frag.content)
|
||||
|
||||
Based on ``docs/specification.md`` ~line 42615 and issue #192.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: CoordinatorConfig | None = None,
|
||||
*,
|
||||
selector: ConfidenceWeightedSelector | None = None,
|
||||
allocator: ProportionalBudgetAllocator | None = None,
|
||||
executor: ParallelStrategyExecutor | None = None,
|
||||
) -> None:
|
||||
self._config = config or CoordinatorConfig()
|
||||
self._selector = selector or ConfidenceWeightedSelector(
|
||||
preference_boost=self._config.preference_boost,
|
||||
)
|
||||
self._allocator = allocator or ProportionalBudgetAllocator(
|
||||
min_useful_budget=self._config.min_useful_budget,
|
||||
)
|
||||
cb = CircuitBreaker(
|
||||
failure_threshold=self._config.circuit_breaker_threshold,
|
||||
)
|
||||
self._executor = executor or ParallelStrategyExecutor(
|
||||
timeout_seconds=self._config.executor_timeout,
|
||||
max_workers=self._config.executor_max_workers,
|
||||
circuit_breaker=cb,
|
||||
)
|
||||
|
||||
@property
|
||||
def config(self) -> CoordinatorConfig:
|
||||
"""Return the coordinator configuration."""
|
||||
return self._config
|
||||
|
||||
@property
|
||||
def selector(self) -> ConfidenceWeightedSelector:
|
||||
"""Return the underlying strategy selector."""
|
||||
return self._selector
|
||||
|
||||
@property
|
||||
def allocator(self) -> ProportionalBudgetAllocator:
|
||||
"""Return the underlying budget allocator."""
|
||||
return self._allocator
|
||||
|
||||
@property
|
||||
def executor(self) -> ParallelStrategyExecutor:
|
||||
"""Return the underlying strategy executor."""
|
||||
return self._executor
|
||||
|
||||
def coordinate(
|
||||
self,
|
||||
request: dict[str, Any],
|
||||
strategies: Sequence[ContextStrategy],
|
||||
budget: ContextBudget,
|
||||
fragments: Sequence[ContextFragment] | None = None,
|
||||
backends: dict[str, Any] | None = None,
|
||||
plan_context: dict[str, Any] | None = None,
|
||||
) -> CoordinationResult:
|
||||
"""Orchestrate strategy selection, budget allocation, and execution.
|
||||
|
||||
Args:
|
||||
request: Context request dict. May include
|
||||
``preferred_strategies`` to boost certain strategies.
|
||||
strategies: Available context strategies to select from.
|
||||
budget: Token budget for the coordination round.
|
||||
fragments: Input fragments for strategies to process.
|
||||
Defaults to an empty sequence.
|
||||
backends: Optional backend configuration (reserved for future
|
||||
use by backend-aware strategies).
|
||||
plan_context: Optional plan context (reserved for future use
|
||||
by plan-aware strategies).
|
||||
|
||||
Returns:
|
||||
A :class:`CoordinationResult` with collected fragments and
|
||||
metadata about the coordination round.
|
||||
"""
|
||||
if fragments is None:
|
||||
fragments = []
|
||||
|
||||
# Merge backend/plan context hints into the request dict for
|
||||
# strategies that inspect these fields.
|
||||
enriched_request = dict(request)
|
||||
if backends is not None:
|
||||
enriched_request["backends"] = backends
|
||||
if plan_context is not None:
|
||||
enriched_request["plan_context"] = plan_context
|
||||
|
||||
# Phase 1a: Strategy selection
|
||||
candidates = self._selector.select(strategies, enriched_request)
|
||||
|
||||
if not candidates:
|
||||
logger.warning(
|
||||
"No strategies selected for request",
|
||||
extra={"request_keys": list(enriched_request.keys())},
|
||||
)
|
||||
return CoordinationResult()
|
||||
|
||||
# Phase 1b: Budget allocation with per-strategy max cap
|
||||
allocations = self._allocator.allocate(candidates, budget.available_tokens)
|
||||
capped_allocations = self._apply_max_caps(allocations)
|
||||
|
||||
# Track circuit-broken strategies
|
||||
cb = self._executor.circuit_breaker
|
||||
circuit_broken = [
|
||||
s.name for s, _c, _t in capped_allocations if cb.is_open(s.name)
|
||||
]
|
||||
|
||||
# Phase 1c: Parallel execution
|
||||
result_fragments = list(
|
||||
self._executor.execute(capped_allocations, fragments, budget)
|
||||
)
|
||||
|
||||
# Derive which strategies actually contributed
|
||||
strategies_used = _extract_strategies_used(
|
||||
capped_allocations, result_fragments, cb
|
||||
)
|
||||
|
||||
allocation_summary = [(s.name, c, t) for s, c, t in capped_allocations]
|
||||
|
||||
logger.info(
|
||||
"Coordination complete",
|
||||
extra={
|
||||
"candidates": len(candidates),
|
||||
"allocations": len(capped_allocations),
|
||||
"fragments_returned": len(result_fragments),
|
||||
"strategies_used": strategies_used,
|
||||
"circuit_broken": circuit_broken,
|
||||
},
|
||||
)
|
||||
|
||||
return CoordinationResult(
|
||||
fragments=result_fragments,
|
||||
strategies_used=strategies_used,
|
||||
allocations=allocation_summary,
|
||||
circuit_broken=circuit_broken,
|
||||
)
|
||||
|
||||
def _apply_max_caps(
|
||||
self,
|
||||
allocations: list[tuple[ContextStrategy, float, int]],
|
||||
) -> list[tuple[ContextStrategy, float, int]]:
|
||||
"""Enforce per-strategy max cap on token allocations.
|
||||
|
||||
When ``per_strategy_max_cap`` is configured, any allocation
|
||||
exceeding the cap is clamped. Excess tokens are redistributed
|
||||
proportionally among uncapped strategies using the
|
||||
largest-remainder method.
|
||||
"""
|
||||
cap = self._config.per_strategy_max_cap
|
||||
if cap is None or not allocations:
|
||||
return allocations
|
||||
|
||||
capped: list[tuple[ContextStrategy, float, int]] = []
|
||||
excess = 0
|
||||
|
||||
# First pass: clamp and collect excess
|
||||
uncapped_indices: list[int] = []
|
||||
for i, (strategy, confidence, tokens) in enumerate(allocations):
|
||||
if tokens > cap:
|
||||
excess += tokens - cap
|
||||
capped.append((strategy, confidence, cap))
|
||||
else:
|
||||
capped.append((strategy, confidence, tokens))
|
||||
uncapped_indices.append(i)
|
||||
|
||||
# Second pass: redistribute excess among uncapped strategies
|
||||
if excess > 0 and uncapped_indices:
|
||||
total_conf = sum(capped[i][1] for i in uncapped_indices)
|
||||
if total_conf > 0:
|
||||
for idx in uncapped_indices:
|
||||
s, c, t = capped[idx]
|
||||
share = int(excess * c / total_conf)
|
||||
new_tokens = min(t + share, cap)
|
||||
capped[idx] = (s, c, new_tokens)
|
||||
else:
|
||||
# Equal split among uncapped
|
||||
share = excess // len(uncapped_indices)
|
||||
for idx in uncapped_indices:
|
||||
s, c, t = capped[idx]
|
||||
capped[idx] = (s, c, min(t + share, cap))
|
||||
|
||||
if excess > 0:
|
||||
logger.debug(
|
||||
"Per-strategy max cap applied",
|
||||
extra={
|
||||
"cap": cap,
|
||||
"excess_redistributed": excess,
|
||||
},
|
||||
)
|
||||
|
||||
return capped
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _extract_strategies_used(
|
||||
allocations: list[tuple[ContextStrategy, float, int]],
|
||||
fragments: list[ContextFragment],
|
||||
circuit_breaker: CircuitBreaker,
|
||||
) -> list[str]:
|
||||
"""Determine which strategies contributed fragments.
|
||||
|
||||
A strategy is considered "used" if it was allocated tokens, was not
|
||||
circuit-broken, and returned at least one fragment (inferred from
|
||||
the allocation being non-empty and the strategy not being disabled).
|
||||
"""
|
||||
# If fragments have strategy_source metadata, use that
|
||||
source_names: set[str] = set()
|
||||
for frag in fragments:
|
||||
if frag.strategy_source:
|
||||
source_names.add(frag.strategy_source)
|
||||
|
||||
if source_names:
|
||||
return sorted(source_names)
|
||||
|
||||
# Fallback: consider all non-circuit-broken, non-zero-budget strategies
|
||||
return sorted(
|
||||
s.name
|
||||
for s, _c, t in allocations
|
||||
if t > 0 and not circuit_breaker.is_open(s.name)
|
||||
)
|
||||
@@ -938,3 +938,22 @@ invalidate_cache # noqa: B018, F821
|
||||
cache_size # noqa: B018, F821
|
||||
register_extension_point # noqa: B018, F821
|
||||
list_extension_points # noqa: B018, F821
|
||||
|
||||
# StrategyCoordinator + FusionEngine (issue #192)
|
||||
StrategyCoordinator # noqa: B018, F821
|
||||
CoordinatorConfig # noqa: B018, F821
|
||||
CoordinationResult # noqa: B018, F821
|
||||
FusionEngine # noqa: B018, F821
|
||||
FusionConfig # noqa: B018, F821
|
||||
FusionResult # noqa: B018, F821
|
||||
coordinate # noqa: B018, F821
|
||||
fuse # noqa: B018, F821
|
||||
overage_guard_enabled # noqa: B018, F821
|
||||
per_strategy_max_cap # noqa: B018, F821
|
||||
circuit_broken # noqa: B018, F821
|
||||
strategies_used # noqa: B018, F821
|
||||
dedup_count # noqa: B018, F821
|
||||
depth_resolved_count # noqa: B018, F821
|
||||
dropped_by_overage_guard # noqa: B018, F821
|
||||
budget_utilization # noqa: B018, F821
|
||||
fusion_input_count # noqa: B018, F821
|
||||
|
||||
Reference in New Issue
Block a user