feat(acms): add ContextFragment and ScoredFragment data models
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 19s
CI / build (pull_request) Successful in 29s
CI / security (pull_request) Successful in 33s
CI / typecheck (pull_request) Successful in 35s
CI / unit_tests (pull_request) Successful in 2m15s
CI / docker (pull_request) Successful in 39s
CI / integration_tests (pull_request) Successful in 3m4s
CI / coverage (pull_request) Successful in 5m1s
CI / lint (push) Successful in 12s
CI / build (push) Successful in 15s
CI / quality (push) Successful in 16s
CI / security (push) Successful in 31s
CI / typecheck (push) Successful in 33s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 3m20s
CI / integration_tests (push) Successful in 4m10s
CI / docker (push) Successful in 1m3s
CI / coverage (push) Successful in 4m20s
CI / benchmark-publish (push) Successful in 16m0s
CI / benchmark-regression (pull_request) Successful in 31m40s

Created ScoredFragment frozen model wrapping ContextFragment with
composite_score, score_breakdown, and rank fields. Added pipeline-
specific fragment models in domain/contexts/ with proper equality
based on uko_uri + detail_depth for deduplication support.

ISSUES CLOSED: #538
This commit was merged in pull request #599.
This commit is contained in:
2026-03-05 07:54:47 +00:00
committed by Forgejo
parent 487e16a9f0
commit 10abf8985e
8 changed files with 1180 additions and 2 deletions
+152
View File
@@ -0,0 +1,152 @@
"""ASV benchmarks for ScoredFragment and ContextFragment pipeline models.
Measures the performance of:
- ScoredFragment creation (with and without breakdown)
- ScoredFragment factory methods
- ScoredFragment equality comparison
- ScoredFragment set-based deduplication
- ContextFragment with strategy_source
"""
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.domain.contexts.fragment import ScoredFragment # noqa: E402
from cleveragents.domain.models.core.context_fragment import ( # noqa: E402
ContextFragment,
FragmentProvenance,
)
_DEFAULT_PROV = FragmentProvenance(resource_uri="bench://default")
def _make_fragment(**kwargs: object) -> ContextFragment:
"""Create a ContextFragment with sensible benchmark defaults."""
defaults: dict[str, object] = {
"uko_node": "bench://file",
"content": "benchmark content",
"token_count": 10,
"provenance": _DEFAULT_PROV,
}
defaults.update(kwargs)
return ContextFragment(**defaults) # type: ignore[arg-type]
class ScoredFragmentCreationSuite:
"""Benchmark ScoredFragment creation throughput."""
def setup(self) -> None:
"""Pre-create fragment for repeated scored-fragment construction."""
self._fragment = _make_fragment()
self._fragment_with_relevance = _make_fragment(relevance_score=0.85)
self._breakdown = {
"relevance": 0.9,
"hierarchy": 0.8,
"recency": 0.95,
}
def time_scored_fragment_minimal(self) -> None:
"""Benchmark creating a ScoredFragment with minimal fields."""
ScoredFragment(
fragment=self._fragment,
composite_score=0.85,
)
def time_scored_fragment_with_breakdown(self) -> None:
"""Benchmark creating a ScoredFragment with full breakdown."""
ScoredFragment(
fragment=self._fragment,
composite_score=0.92,
score_breakdown=self._breakdown,
rank=1,
)
def time_scored_fragment_from_relevance(self) -> None:
"""Benchmark ScoredFragment.from_relevance factory."""
ScoredFragment.from_relevance(self._fragment_with_relevance)
def time_scored_fragment_from_factory(self) -> None:
"""Benchmark ScoredFragment.from_fragment factory."""
ScoredFragment.from_fragment(
self._fragment,
composite_score=0.6,
score_breakdown=self._breakdown,
rank=3,
)
class ScoredFragmentEqualitySuite:
"""Benchmark ScoredFragment equality and hashing."""
def setup(self) -> None:
"""Pre-create scored fragments for comparison benchmarks."""
frag_a = _make_fragment(
uko_node="bench://file/a",
content="A",
detail_depth=3,
)
frag_b = _make_fragment(
uko_node="bench://file/a",
content="B",
detail_depth=3,
)
self._scored_a = ScoredFragment.from_fragment(frag_a, composite_score=0.5)
self._scored_b = ScoredFragment.from_fragment(frag_b, composite_score=0.9)
# Build a list of 100 fragments (50 unique uko_node values)
self._scored_list = [
ScoredFragment.from_fragment(
_make_fragment(
uko_node=f"bench://file/{i % 50}",
content=f"content {i}",
detail_depth=3,
),
composite_score=round((i % 10) / 10, 1),
)
for i in range(100)
]
def time_equality_check(self) -> None:
"""Benchmark equality comparison."""
_ = self._scored_a == self._scored_b
def time_hash_computation(self) -> None:
"""Benchmark hash computation."""
hash(self._scored_a)
def time_set_deduplication_100(self) -> None:
"""Benchmark set-based deduplication of 100 scored fragments."""
set(self._scored_list)
class ContextFragmentStrategySuite:
"""Benchmark ContextFragment with strategy_source field."""
def time_fragment_with_strategy_source(self) -> None:
"""Benchmark creating a ContextFragment with strategy_source."""
ContextFragment(
uko_node="bench://file",
content="benchmark content",
token_count=10,
provenance=_DEFAULT_PROV,
strategy_source="keyword-search",
)
def time_fragment_default_strategy_source(self) -> None:
"""Benchmark creating a ContextFragment with default strategy_source."""
ContextFragment(
uko_node="bench://file",
content="benchmark content",
token_count=10,
provenance=_DEFAULT_PROV,
)
+136
View File
@@ -0,0 +1,136 @@
@phase2 @acms @context_fragment_models
Feature: ScoredFragment and ContextFragment pipeline models
As a CleverAgents developer
I want pipeline-specific fragment models with scoring and deduplication
So that the ACMS context assembly pipeline can rank, deduplicate, and pack fragments
# ---------------------------------------------------------------------------
# ScoredFragment Construction
# ---------------------------------------------------------------------------
@scored_fragment
Scenario: Create a ScoredFragment with required fields
Given a context fragment with uko_node "project://app/main.py" and content "hello" and token_count 10
When I create a scored fragment with composite_score 0.85
Then the scored fragment composite_score should be 0.85
And the scored fragment rank should be 0
And the scored fragment score_breakdown should be empty
And the scored fragment uko_node should be "project://app/main.py"
And the scored fragment detail_depth should be 0
And the scored fragment token_count should be 10
@scored_fragment
Scenario: Create a ScoredFragment with all fields
Given a context fragment with uko_node "project://app/io.py" and content "async IO" and token_count 50
When I create a scored fragment with composite_score 0.92 and rank 1 and breakdown:
| component | score |
| relevance | 0.9 |
| hierarchy | 0.8 |
| recency | 0.95 |
Then the scored fragment composite_score should be 0.92
And the scored fragment rank should be 1
And the scored fragment breakdown "relevance" should be 0.9
And the scored fragment breakdown "hierarchy" should be 0.8
And the scored fragment breakdown "recency" should be 0.95
@scored_fragment
Scenario: Create a ScoredFragment from relevance factory
Given a context fragment with uko_node "project://app/util.py" and content "utils" and token_count 20 and relevance 0.75
When I create a scored fragment from relevance
Then the scored fragment composite_score should be 0.75
And the scored fragment breakdown "relevance" should be 0.75
And the scored fragment rank should be 0
@scored_fragment
Scenario: Create a ScoredFragment from fragment factory
Given a context fragment with uko_node "project://app/core.py" and content "core" and token_count 30
When I create a scored fragment from factory with score 0.6 and rank 3
Then the scored fragment composite_score should be 0.6
And the scored fragment rank should be 3
# ---------------------------------------------------------------------------
# ScoredFragment — Equality and Hashing
# ---------------------------------------------------------------------------
@scored_fragment @equality
Scenario: ScoredFragments with same uko_node and detail_depth are equal
Given two scored fragments with uko_node "project://app/main.py" and detail_depth 3
Then the two scored fragments should be equal
And the two scored fragments should have the same hash
@scored_fragment @equality
Scenario: ScoredFragments with different uko_node are not equal
Given a scored fragment with uko_node "project://app/main.py" and detail_depth 3
And another scored fragment with uko_node "project://app/other.py" and detail_depth 3
Then the two scored fragments should not be equal
@scored_fragment @equality
Scenario: ScoredFragments with different detail_depth are not equal
Given a scored fragment with uko_node "project://app/main.py" and detail_depth 3
And another scored fragment with uko_node "project://app/main.py" and detail_depth 5
Then the two scored fragments should not be equal
@scored_fragment @equality
Scenario: ScoredFragments can be deduplicated via set
Given three scored fragments where two share uko_node "project://app/main.py" detail_depth 3
When I add them to a set
Then the set should contain 2 unique scored fragments
@scored_fragment @equality
Scenario: ScoredFragments with same identity but different scores are equal
Given a scored fragment with uko_node "project://app/main.py" and detail_depth 2 and score 0.5
And another scored fragment with uko_node "project://app/main.py" and detail_depth 2 and score 0.9
Then the two scored fragments should be equal
And the two scored fragments should have the same hash
# ---------------------------------------------------------------------------
# ScoredFragment — Immutability
# ---------------------------------------------------------------------------
@scored_fragment @immutability
Scenario: ScoredFragment is frozen
Given a context fragment with uko_node "project://app/main.py" and content "hello" and token_count 10
When I create a scored fragment with composite_score 0.85
Then modifying the scored fragment composite_score should raise an error
# ---------------------------------------------------------------------------
# ScoredFragment — Validation
# ---------------------------------------------------------------------------
@scored_fragment @validation
Scenario: Invalid composite_score above 1.0 rejected
Given a context fragment with uko_node "project://app/main.py" and content "hello" and token_count 10
When I create a scored fragment with composite_score 1.5
Then a validation error should be raised
@scored_fragment @validation
Scenario: Invalid composite_score below 0.0 rejected
Given a context fragment with uko_node "project://app/main.py" and content "hello" and token_count 10
When I create a scored fragment with composite_score -0.1
Then a validation error should be raised
@scored_fragment @validation
Scenario: Invalid score_breakdown component rejected
Given a context fragment with uko_node "project://app/main.py" and content "hello" and token_count 10
When I create a scored fragment with an invalid breakdown component 1.5
Then a validation error should be raised
@scored_fragment @validation
Scenario: Negative rank rejected
Given a context fragment with uko_node "project://app/main.py" and content "hello" and token_count 10
When I create a scored fragment with composite_score 0.5 and rank -1
Then a validation error should be raised
# ---------------------------------------------------------------------------
# ContextFragment — strategy_source field
# ---------------------------------------------------------------------------
@context_fragment @strategy_source
Scenario: ContextFragment has strategy_source field with default
Given a context fragment with uko_node "project://app/main.py" and content "hello" and token_count 10
Then the fragment strategy_source should be empty
@context_fragment @strategy_source
Scenario: ContextFragment strategy_source can be set
Given a context fragment with uko_node "project://app/main.py" and content "hello" and token_count 10 and strategy_source "keyword-search"
Then the fragment strategy_source should be "keyword-search"
@@ -0,0 +1,405 @@
"""Step definitions for features/context_fragment_models.feature.
Tests the ScoredFragment pipeline model and ContextFragment strategy_source
field directly in-memory -- no database required.
"""
from __future__ import annotations
from typing import Any
from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError
from cleveragents.domain.contexts.fragment import ScoredFragment
from cleveragents.domain.models.core.context_fragment import (
ContextFragment,
FragmentProvenance,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
_DEFAULT_PROVENANCE = FragmentProvenance(resource_uri="test://default")
def _make_fragment(**kwargs: Any) -> ContextFragment:
"""Create a ContextFragment with sensible test defaults."""
kwargs.setdefault("uko_node", "test://default")
kwargs.setdefault("content", "test content")
kwargs.setdefault("token_count", 0)
kwargs.setdefault("provenance", _DEFAULT_PROVENANCE)
return ContextFragment(**kwargs)
# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------
@given(
'a context fragment with uko_node "{uko}" and content "{content}"'
" and token_count {tokens:d} and relevance {rel:g}"
)
def step_given_fragment_with_relevance(
context: Context,
uko: str,
content: str,
tokens: int,
rel: float,
) -> None:
context.fragment = _make_fragment(
uko_node=uko,
content=content,
token_count=tokens,
relevance_score=rel,
)
@given(
'a context fragment with uko_node "{uko}" and content "{content}"'
' and token_count {tokens:d} and strategy_source "{strategy}"'
)
def step_given_fragment_with_strategy(
context: Context,
uko: str,
content: str,
tokens: int,
strategy: str,
) -> None:
context.fragment = _make_fragment(
uko_node=uko,
content=content,
token_count=tokens,
strategy_source=strategy,
)
@given('two scored fragments with uko_node "{uko}" and detail_depth {depth:d}')
def step_given_two_same_scored_fragments(
context: Context,
uko: str,
depth: int,
) -> None:
frag_a = _make_fragment(
uko_node=uko,
content="content A",
detail_depth=depth,
token_count=10,
)
frag_b = _make_fragment(
uko_node=uko,
content="content B",
detail_depth=depth,
token_count=20,
)
context.scored_a = ScoredFragment.from_fragment(frag_a, composite_score=0.5)
context.scored_b = ScoredFragment.from_fragment(frag_b, composite_score=0.9)
@given('a scored fragment with uko_node "{uko}" and detail_depth {depth:d}')
def step_given_scored_fragment_first(
context: Context,
uko: str,
depth: int,
) -> None:
frag = _make_fragment(
uko_node=uko,
content="content",
detail_depth=depth,
token_count=10,
)
context.scored_a = ScoredFragment.from_fragment(frag, composite_score=0.5)
@given('another scored fragment with uko_node "{uko}" and detail_depth {depth:d}')
def step_given_scored_fragment_second(
context: Context,
uko: str,
depth: int,
) -> None:
frag = _make_fragment(
uko_node=uko,
content="content",
detail_depth=depth,
token_count=10,
)
context.scored_b = ScoredFragment.from_fragment(frag, composite_score=0.7)
@given(
'a scored fragment with uko_node "{uko}" and detail_depth {depth:d}'
" and score {score:g}"
)
def step_given_scored_fragment_with_score(
context: Context,
uko: str,
depth: int,
score: float,
) -> None:
frag = _make_fragment(
uko_node=uko,
content="content",
detail_depth=depth,
token_count=10,
)
context.scored_a = ScoredFragment.from_fragment(frag, composite_score=score)
@given(
'another scored fragment with uko_node "{uko}" and detail_depth {depth:d}'
" and score {score:g}"
)
def step_given_another_scored_fragment_with_score(
context: Context,
uko: str,
depth: int,
score: float,
) -> None:
frag = _make_fragment(
uko_node=uko,
content="content",
detail_depth=depth,
token_count=10,
)
context.scored_b = ScoredFragment.from_fragment(frag, composite_score=score)
@given('three scored fragments where two share uko_node "{uko}" detail_depth {depth:d}')
def step_given_three_scored_fragments(
context: Context,
uko: str,
depth: int,
) -> None:
frag_a = _make_fragment(
uko_node=uko,
content="content A",
detail_depth=depth,
token_count=10,
)
frag_b = _make_fragment(
uko_node=uko,
content="content B",
detail_depth=depth,
token_count=20,
)
frag_c = _make_fragment(
uko_node="project://app/other.py",
content="content C",
detail_depth=depth,
token_count=30,
)
context.scored_list = [
ScoredFragment.from_fragment(frag_a, composite_score=0.5),
ScoredFragment.from_fragment(frag_b, composite_score=0.9),
ScoredFragment.from_fragment(frag_c, composite_score=0.7),
]
# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------
@when("I create a scored fragment with composite_score {score:g}")
def step_when_create_scored(context: Context, score: float) -> None:
try:
context.scored = ScoredFragment(
fragment=context.fragment,
composite_score=score,
)
context.error = None
except (ValidationError, ValueError) as exc:
context.scored = None
context.error = exc
@when(
"I create a scored fragment with composite_score {score:g}"
" and rank {rank:d} and breakdown:"
)
def step_when_create_scored_with_breakdown(
context: Context,
score: float,
rank: int,
) -> None:
breakdown: dict[str, float] = {}
assert context.table is not None
for row in context.table:
breakdown[row["component"]] = float(row["score"])
context.scored = ScoredFragment(
fragment=context.fragment,
composite_score=score,
score_breakdown=breakdown,
rank=rank,
)
@when("I create a scored fragment from relevance")
def step_when_create_from_relevance(context: Context) -> None:
context.scored = ScoredFragment.from_relevance(context.fragment)
@when("I create a scored fragment from factory with score {score:g} and rank {rank:d}")
def step_when_create_from_factory(
context: Context,
score: float,
rank: int,
) -> None:
context.scored = ScoredFragment.from_fragment(
context.fragment,
composite_score=score,
rank=rank,
)
@when("I create a scored fragment with an invalid breakdown component {score:g}")
def step_when_create_invalid_breakdown(context: Context, score: float) -> None:
try:
context.scored = ScoredFragment(
fragment=context.fragment,
composite_score=0.5,
score_breakdown={"bad": score},
)
context.error = None
except (ValidationError, ValueError) as exc:
context.scored = None
context.error = exc
@when("I create a scored fragment with composite_score {score:g} and rank {rank:d}")
def step_when_create_scored_with_rank(
context: Context,
score: float,
rank: int,
) -> None:
try:
context.scored = ScoredFragment(
fragment=context.fragment,
composite_score=score,
rank=rank,
)
context.error = None
except (ValidationError, ValueError) as exc:
context.scored = None
context.error = exc
@when("I add them to a set")
def step_when_add_to_set(context: Context) -> None:
context.scored_set = set(context.scored_list)
# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------
@then("the scored fragment composite_score should be {expected:g}")
def step_then_composite_score(context: Context, expected: float) -> None:
assert context.scored is not None, "No scored fragment created"
assert context.scored.composite_score == expected, (
f"Expected composite_score={expected}, got {context.scored.composite_score}"
)
@then("the scored fragment rank should be {expected:d}")
def step_then_rank(context: Context, expected: int) -> None:
assert context.scored is not None, "No scored fragment created"
assert context.scored.rank == expected, (
f"Expected rank={expected}, got {context.scored.rank}"
)
@then("the scored fragment score_breakdown should be empty")
def step_then_breakdown_empty(context: Context) -> None:
assert context.scored is not None, "No scored fragment created"
assert context.scored.score_breakdown == {}, (
f"Expected empty breakdown, got {context.scored.score_breakdown}"
)
@then('the scored fragment uko_node should be "{expected}"')
def step_then_scored_uko_node(context: Context, expected: str) -> None:
assert context.scored is not None, "No scored fragment created"
assert context.scored.uko_node == expected, (
f"Expected uko_node={expected}, got {context.scored.uko_node}"
)
@then("the scored fragment detail_depth should be {expected:d}")
def step_then_scored_detail_depth(context: Context, expected: int) -> None:
assert context.scored is not None, "No scored fragment created"
assert context.scored.detail_depth == expected, (
f"Expected detail_depth={expected}, got {context.scored.detail_depth}"
)
@then("the scored fragment token_count should be {expected:d}")
def step_then_scored_token_count(context: Context, expected: int) -> None:
assert context.scored is not None, "No scored fragment created"
assert context.scored.token_count == expected, (
f"Expected token_count={expected}, got {context.scored.token_count}"
)
@then('the scored fragment breakdown "{key}" should be {expected:g}')
def step_then_breakdown_value(context: Context, key: str, expected: float) -> None:
assert context.scored is not None, "No scored fragment created"
assert key in context.scored.score_breakdown, (
f"Key '{key}' not in score_breakdown: {context.scored.score_breakdown}"
)
actual = context.scored.score_breakdown[key]
assert actual == expected, f"Expected breakdown['{key}']={expected}, got {actual}"
@then("the two scored fragments should be equal")
def step_then_scored_equal(context: Context) -> None:
assert context.scored_a == context.scored_b, "Expected scored fragments to be equal"
@then("the two scored fragments should not be equal")
def step_then_scored_not_equal(context: Context) -> None:
assert context.scored_a != context.scored_b, (
"Expected scored fragments to NOT be equal"
)
@then("the two scored fragments should have the same hash")
def step_then_scored_same_hash(context: Context) -> None:
assert hash(context.scored_a) == hash(context.scored_b), (
"Expected scored fragments to have the same hash"
)
@then("the set should contain {expected:d} unique scored fragments")
def step_then_set_size(context: Context, expected: int) -> None:
assert len(context.scored_set) == expected, (
f"Expected {expected} unique fragments, got {len(context.scored_set)}"
)
@then("modifying the scored fragment composite_score should raise an error")
def step_then_frozen(context: Context) -> None:
try:
context.scored.composite_score = 0.99 # type: ignore[misc]
raise AssertionError("Expected ValidationError for frozen model")
except ValidationError:
pass
@then("the fragment strategy_source should be empty")
def step_then_strategy_source_empty(context: Context) -> None:
assert context.fragment.strategy_source == "", (
f"Expected empty strategy_source, got '{context.fragment.strategy_source}'"
)
@then('the fragment strategy_source should be "{expected}"')
def step_then_strategy_source(context: Context, expected: str) -> None:
assert context.fragment.strategy_source == expected, (
f"Expected strategy_source='{expected}', "
f"got '{context.fragment.strategy_source}'"
)
+65
View File
@@ -0,0 +1,65 @@
*** Settings ***
Documentation Integration smoke tests for ScoredFragment and ContextFragment pipeline models
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER} ${CURDIR}/helper_context_fragment_models.py
*** Test Cases ***
Create ScoredFragment With Defaults
[Documentation] Create a ScoredFragment and verify default fields
${result}= Run Process ${PYTHON} ${HELPER} scored-create cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} scored-create-ok
Create ScoredFragment With Breakdown
[Documentation] Create a ScoredFragment with full score breakdown
${result}= Run Process ${PYTHON} ${HELPER} scored-breakdown cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} scored-breakdown-ok
ScoredFragment Equality By Identity
[Documentation] Verify equality based on uko_node + detail_depth
${result}= Run Process ${PYTHON} ${HELPER} scored-equality cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} scored-equality-ok
ScoredFragment Deduplication Via Set
[Documentation] Verify set-based deduplication works
${result}= Run Process ${PYTHON} ${HELPER} scored-dedup cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} scored-dedup-ok
ScoredFragment From Relevance Factory
[Documentation] Create ScoredFragment using from_relevance factory
${result}= Run Process ${PYTHON} ${HELPER} scored-from-relevance cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} scored-from-relevance-ok
ScoredFragment Immutability
[Documentation] Verify ScoredFragment is frozen
${result}= Run Process ${PYTHON} ${HELPER} scored-frozen cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} scored-frozen-ok
ContextFragment Strategy Source
[Documentation] Verify strategy_source field on ContextFragment
${result}= Run Process ${PYTHON} ${HELPER} strategy-source cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} strategy-source-ok
+227
View File
@@ -0,0 +1,227 @@
"""Robot Framework helper for ScoredFragment and ContextFragment integration tests.
Provides a CLI-style interface for Robot to invoke pipeline model
operations and verify results. Exit code 0 = success, 1 = failure.
Usage:
python robot/helper_context_fragment_models.py scored-create
python robot/helper_context_fragment_models.py scored-breakdown
python robot/helper_context_fragment_models.py scored-equality
python robot/helper_context_fragment_models.py scored-dedup
python robot/helper_context_fragment_models.py scored-from-relevance
python robot/helper_context_fragment_models.py scored-frozen
python robot/helper_context_fragment_models.py strategy-source
"""
from __future__ import annotations
import sys
from collections.abc import Callable
from pathlib import Path
from pydantic import ValidationError
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.domain.contexts.fragment import ScoredFragment # noqa: E402
from cleveragents.domain.models.core.context_fragment import ( # noqa: E402
ContextFragment,
FragmentProvenance,
)
_DEFAULT_PROV = FragmentProvenance(resource_uri="test://robot")
def _make_fragment(**kwargs: object) -> ContextFragment:
"""Create a ContextFragment with sensible test defaults."""
defaults: dict[str, object] = {
"uko_node": "test://default",
"content": "test content",
"token_count": 10,
"provenance": _DEFAULT_PROV,
}
defaults.update(kwargs)
return ContextFragment(**defaults) # type: ignore[arg-type]
def _cmd_scored_create() -> int:
"""Create a ScoredFragment and verify defaults."""
frag = _make_fragment(uko_node="project://app/main.py", content="hello")
scored = ScoredFragment(fragment=frag, composite_score=0.85)
if scored.composite_score != 0.85:
print(f"fail: expected composite_score=0.85, got {scored.composite_score}")
return 1
if scored.rank != 0:
print(f"fail: expected rank=0, got {scored.rank}")
return 1
if scored.score_breakdown != {}:
print(f"fail: expected empty breakdown, got {scored.score_breakdown}")
return 1
if scored.uko_node != "project://app/main.py":
print(f"fail: expected uko_node=project://app/main.py, got {scored.uko_node}")
return 1
print("scored-create-ok")
return 0
def _cmd_scored_breakdown() -> int:
"""Create a ScoredFragment with full breakdown."""
frag = _make_fragment(uko_node="project://app/io.py", content="async IO")
breakdown = {"relevance": 0.9, "hierarchy": 0.8, "recency": 0.95}
scored = ScoredFragment(
fragment=frag,
composite_score=0.92,
score_breakdown=breakdown,
rank=1,
)
if scored.composite_score != 0.92:
print(f"fail: expected composite_score=0.92, got {scored.composite_score}")
return 1
if scored.rank != 1:
print(f"fail: expected rank=1, got {scored.rank}")
return 1
if scored.score_breakdown.get("relevance") != 0.9:
print(f"fail: expected relevance=0.9, got {scored.score_breakdown}")
return 1
print("scored-breakdown-ok")
return 0
def _cmd_scored_equality() -> int:
"""Verify equality based on uko_node + detail_depth."""
frag_a = _make_fragment(
uko_node="project://app/main.py",
content="A",
detail_depth=3,
)
frag_b = _make_fragment(
uko_node="project://app/main.py",
content="B",
detail_depth=3,
)
sa = ScoredFragment.from_fragment(frag_a, composite_score=0.5)
sb = ScoredFragment.from_fragment(frag_b, composite_score=0.9)
if sa != sb:
print("fail: expected equal scored fragments")
return 1
if hash(sa) != hash(sb):
print("fail: expected same hash")
return 1
# Different uko_node should be not equal
frag_c = _make_fragment(
uko_node="project://app/other.py",
content="C",
detail_depth=3,
)
sc = ScoredFragment.from_fragment(frag_c, composite_score=0.5)
if sa == sc:
print("fail: expected different scored fragments")
return 1
print("scored-equality-ok")
return 0
def _cmd_scored_dedup() -> int:
"""Verify set-based deduplication."""
frag_a = _make_fragment(
uko_node="project://app/main.py",
content="A",
detail_depth=3,
)
frag_b = _make_fragment(
uko_node="project://app/main.py",
content="B",
detail_depth=3,
)
frag_c = _make_fragment(
uko_node="project://app/other.py",
content="C",
detail_depth=3,
)
scored_set = {
ScoredFragment.from_fragment(frag_a, composite_score=0.5),
ScoredFragment.from_fragment(frag_b, composite_score=0.9),
ScoredFragment.from_fragment(frag_c, composite_score=0.7),
}
if len(scored_set) != 2:
print(f"fail: expected 2 unique, got {len(scored_set)}")
return 1
print("scored-dedup-ok")
return 0
def _cmd_scored_from_relevance() -> int:
"""Create ScoredFragment using from_relevance factory."""
frag = _make_fragment(
uko_node="project://app/util.py",
content="utils",
relevance_score=0.75,
)
scored = ScoredFragment.from_relevance(frag)
if scored.composite_score != 0.75:
print(f"fail: expected composite_score=0.75, got {scored.composite_score}")
return 1
if scored.score_breakdown.get("relevance") != 0.75:
print(f"fail: expected relevance=0.75, got {scored.score_breakdown}")
return 1
print("scored-from-relevance-ok")
return 0
def _cmd_scored_frozen() -> int:
"""Verify ScoredFragment is frozen."""
frag = _make_fragment()
scored = ScoredFragment(fragment=frag, composite_score=0.5)
try:
scored.composite_score = 0.99 # type: ignore[misc]
print("fail: expected error when modifying frozen model")
return 1
except ValidationError:
pass
print("scored-frozen-ok")
return 0
def _cmd_strategy_source() -> int:
"""Verify strategy_source field on ContextFragment."""
# Default should be empty string
frag = _make_fragment()
if frag.strategy_source != "":
print(f"fail: expected empty strategy_source, got '{frag.strategy_source}'")
return 1
# Explicit value
frag2 = _make_fragment(strategy_source="keyword-search")
if frag2.strategy_source != "keyword-search":
print(
f"fail: expected strategy_source='keyword-search', "
f"got '{frag2.strategy_source}'"
)
return 1
print("strategy-source-ok")
return 0
_COMMANDS: dict[str, Callable[[], int]] = {
"scored-create": _cmd_scored_create,
"scored-breakdown": _cmd_scored_breakdown,
"scored-equality": _cmd_scored_equality,
"scored-dedup": _cmd_scored_dedup,
"scored-from-relevance": _cmd_scored_from_relevance,
"scored-frozen": _cmd_scored_frozen,
"strategy-source": _cmd_strategy_source,
}
def main() -> int:
"""Dispatch to the requested sub-command."""
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
valid = ", ".join(sorted(_COMMANDS))
print(f"Usage: {sys.argv[0]} <{valid}>", file=sys.stderr)
return 1
return _COMMANDS[sys.argv[1]]()
if __name__ == "__main__":
raise SystemExit(main())
+7 -2
View File
@@ -1,6 +1,11 @@
"""Contexts domain module.
Contains domain models and business logic related to plan contexts.
Contains pipeline-specific domain models for the ACMS context assembly
pipeline, including ``ScoredFragment`` for scored/ranked fragments.
"""
__all__ = []
from cleveragents.domain.contexts.fragment import ScoredFragment
__all__ = [
"ScoredFragment",
]
@@ -0,0 +1,180 @@
"""Pipeline-specific fragment models for the ACMS context assembly pipeline.
Provides ``ScoredFragment``, a frozen Pydantic value object that wraps a
``ContextFragment`` with composite scoring information produced by the
``FragmentScorer`` pipeline stage.
Equality and hashing are based on ``uko_node`` + ``detail_depth`` to support
deduplication in the pipeline without relying on object identity.
Based on ``docs/specification.md`` ~line 42826 (ScoredFragment).
"""
from __future__ import annotations
from pydantic import BaseModel, Field, field_validator, model_validator
from cleveragents.domain.models.core.context_fragment import ContextFragment
class ScoredFragment(BaseModel, frozen=True):
"""A ``ContextFragment`` annotated with a composite score.
Produced by the ``FragmentScorer`` pipeline stage and consumed by
``BudgetPacker`` for ranking during budget packing. The model is
frozen so that scored fragments are safe to share across pipeline
stages without defensive copies.
Equality and hashing are based on ``fragment.uko_node`` and
``fragment.detail_depth`` so that two ``ScoredFragment`` instances
wrapping the same logical content are considered duplicates
regardless of scoring differences.
Based on ``docs/specification.md`` ~line 42826.
"""
fragment: ContextFragment = Field(
...,
description="The underlying context fragment being scored.",
)
composite_score: float = Field(
...,
ge=0.0,
le=1.0,
description="Overall composite ranking score (0.0-1.0).",
)
score_breakdown: dict[str, float] = Field(
default_factory=dict,
description=(
"Component-level score breakdown. Keys are component names "
"(e.g. 'relevance', 'hierarchy', 'recency') and values are "
"individual scores in 0.0-1.0."
),
)
rank: int = Field(
default=0,
ge=0,
description=(
"Ordinal rank assigned after scoring (0 = unranked / not yet "
"assigned). Lower values indicate higher priority."
),
)
@field_validator("score_breakdown")
@classmethod
def _validate_score_breakdown(cls, v: dict[str, float]) -> dict[str, float]:
"""Validate all component scores are in [0.0, 1.0] and deep-copy."""
for name, score in v.items():
if score < 0.0 or score > 1.0:
msg = (
f"score_breakdown['{name}'] must be between 0.0 and 1.0, "
f"got {score}"
)
raise ValueError(msg)
return dict(v)
@model_validator(mode="after")
def _check_composite_within_components(self) -> ScoredFragment:
"""Warn-free validation: composite_score is present and valid.
The composite score does not need to equal the average of
components strategies may apply custom weighting but it
must be in range, which is already enforced by the field
constraint.
"""
return self
# ------------------------------------------------------------------
# Equality / hashing based on uko_node + detail_depth
# ------------------------------------------------------------------
def __eq__(self, other: object) -> bool:
"""Two scored fragments are equal when their underlying fragments
share the same ``uko_node`` and ``detail_depth``."""
if not isinstance(other, ScoredFragment):
return NotImplemented
return (
self.fragment.uko_node == other.fragment.uko_node
and self.fragment.detail_depth == other.fragment.detail_depth
)
def __hash__(self) -> int:
"""Hash based on ``uko_node`` + ``detail_depth`` for set/dict use."""
return hash((self.fragment.uko_node, self.fragment.detail_depth))
# ------------------------------------------------------------------
# Factory helpers
# ------------------------------------------------------------------
@staticmethod
def from_fragment(
fragment: ContextFragment,
composite_score: float,
score_breakdown: dict[str, float] | None = None,
rank: int = 0,
) -> ScoredFragment:
"""Create a ``ScoredFragment`` from an existing ``ContextFragment``.
This is the preferred construction method for pipeline stages.
Args:
fragment: The context fragment to wrap.
composite_score: The overall composite score (0.0-1.0).
score_breakdown: Optional component-level breakdown.
rank: Optional ordinal rank (0 = unranked).
Returns:
A new frozen ``ScoredFragment`` instance.
"""
return ScoredFragment(
fragment=fragment,
composite_score=composite_score,
score_breakdown=score_breakdown if score_breakdown is not None else {},
rank=rank,
)
@staticmethod
def from_relevance(
fragment: ContextFragment,
rank: int = 0,
) -> ScoredFragment:
"""Create a ``ScoredFragment`` using only the fragment's relevance_score.
Convenience factory for simple scoring where the composite score
equals the fragment's built-in ``relevance_score``.
Args:
fragment: The context fragment to wrap.
rank: Optional ordinal rank (0 = unranked).
Returns:
A new frozen ``ScoredFragment`` with relevance as sole component.
"""
return ScoredFragment(
fragment=fragment,
composite_score=fragment.relevance_score,
score_breakdown={"relevance": fragment.relevance_score},
rank=rank,
)
# ------------------------------------------------------------------
# Convenience properties
# ------------------------------------------------------------------
@property
def uko_node(self) -> str:
"""Shortcut to the underlying fragment's ``uko_node``."""
return self.fragment.uko_node
@property
def detail_depth(self) -> int:
"""Shortcut to the underlying fragment's ``detail_depth``."""
return self.fragment.detail_depth
@property
def token_count(self) -> int:
"""Shortcut to the underlying fragment's ``token_count``."""
return self.fragment.token_count
@@ -102,6 +102,14 @@ class ContextFragment(BaseModel, frozen=True):
description="Trace back to resource and location",
)
strategy_source: str = Field(
default="",
description=(
"Name of the strategy that produced this fragment. Empty string "
"means the strategy is unknown or not yet assigned."
),
)
tier: Literal["hot", "warm", "cold"] = Field(default="warm")
metadata: dict[str, str] = Field(default_factory=dict)