context-engineering-core 0.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- context_engine/__init__.py +15 -0
- context_engine/__main__.py +3 -0
- context_engine/_sqlite.py +19 -0
- context_engine/_windows_security.py +125 -0
- context_engine/_windows_storage.py +110 -0
- context_engine/adapters.py +148 -0
- context_engine/answers.py +90 -0
- context_engine/budget.py +93 -0
- context_engine/cli.py +222 -0
- context_engine/config.py +193 -0
- context_engine/context.py +116 -0
- context_engine/demo.py +98 -0
- context_engine/errors.py +91 -0
- context_engine/evaluation/__init__.py +1 -0
- context_engine/evaluation/commands.py +90 -0
- context_engine/evaluation/corpus.py +247 -0
- context_engine/evaluation/data/freeze.json +26 -0
- context_engine/evaluation/data/ground_truth.json +287 -0
- context_engine/evaluation/data/pin_retention.json +8 -0
- context_engine/evaluation/data/protocol.json +122 -0
- context_engine/evaluation/data/scenario.json +1226 -0
- context_engine/evaluation/data/summary_fixture.json +7 -0
- context_engine/evaluation/execution.py +538 -0
- context_engine/evaluation/fixtures.py +32 -0
- context_engine/evaluation/grading.py +62 -0
- context_engine/evaluation/harness.py +165 -0
- context_engine/evaluation/heldout/__init__.py +260 -0
- context_engine/evaluation/heldout/__main__.py +40 -0
- context_engine/evaluation/heldout/data/ground_truth.json +18 -0
- context_engine/evaluation/heldout/data/manifest.json +9 -0
- context_engine/evaluation/heldout/data/protocol.json +14 -0
- context_engine/evaluation/heldout/data/scenarios.json +18 -0
- context_engine/evaluation/heldout/data/scoring_manifest.json +4 -0
- context_engine/evaluation/heldout/data/scoring_protocol.json +12 -0
- context_engine/evaluation/heldout/scoring.py +176 -0
- context_engine/evaluation/protocol.py +264 -0
- context_engine/evaluation/reporting.py +294 -0
- context_engine/evaluation/results.py +331 -0
- context_engine/evaluation/run_commands.py +146 -0
- context_engine/evaluation/strategies.py +261 -0
- context_engine/inspection.py +266 -0
- context_engine/layers/__init__.py +18 -0
- context_engine/layers/cap.py +99 -0
- context_engine/layers/common.py +162 -0
- context_engine/layers/pin.py +68 -0
- context_engine/layers/retrieve.py +233 -0
- context_engine/layers/summarize.py +202 -0
- context_engine/layers/window.py +29 -0
- context_engine/mcp_server.py +267 -0
- context_engine/memory/__init__.py +5 -0
- context_engine/memory/codec.py +91 -0
- context_engine/memory/demo.py +90 -0
- context_engine/memory/store.py +770 -0
- context_engine/models.py +384 -0
- context_engine/pipeline.py +383 -0
- context_engine/providers/__init__.py +1 -0
- context_engine/providers/client.py +301 -0
- context_engine/providers/commands.py +138 -0
- context_engine/providers/contracts.py +324 -0
- context_engine/providers/credentials.py +66 -0
- context_engine/providers/memory.py +112 -0
- context_engine/providers/store.py +408 -0
- context_engine/providers/transport.py +195 -0
- context_engine/providers/windows_credentials.py +23 -0
- context_engine/py.typed +0 -0
- context_engine/service/__init__.py +1 -0
- context_engine/service/__main__.py +68 -0
- context_engine/service/app.py +404 -0
- context_engine/service/auth.py +132 -0
- context_engine/service/client.py +52 -0
- context_engine/service/control.py +134 -0
- context_engine/service/demo.py +151 -0
- context_engine/service/integrations.py +28 -0
- context_engine/service/schemas.py +59 -0
- context_engine/service/worker.py +59 -0
- context_engine/service/workers.py +156 -0
- context_engine/summaries.py +196 -0
- context_engine/tokens.py +171 -0
- context_engine/work.py +46 -0
- context_engineering_core-0.12.0.dist-info/METADATA +203 -0
- context_engineering_core-0.12.0.dist-info/RECORD +84 -0
- context_engineering_core-0.12.0.dist-info/WHEEL +4 -0
- context_engineering_core-0.12.0.dist-info/entry_points.txt +3 -0
- context_engineering_core-0.12.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Five independently callable context layers. No inference is performed."""
|
|
2
|
+
|
|
3
|
+
from .cap import CapLayer
|
|
4
|
+
from .pin import PinLayer
|
|
5
|
+
from .retrieve import RetrieveLayer
|
|
6
|
+
from .summarize import FrozenSummaryPolicy, PrefixSummaryPolicy, SummaryLayer, SummarySnapshot
|
|
7
|
+
from .window import WindowLayer
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"CapLayer",
|
|
11
|
+
"PinLayer",
|
|
12
|
+
"RetrieveLayer",
|
|
13
|
+
"WindowLayer",
|
|
14
|
+
"SummaryLayer",
|
|
15
|
+
"FrozenSummaryPolicy",
|
|
16
|
+
"PrefixSummaryPolicy",
|
|
17
|
+
"SummarySnapshot",
|
|
18
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Deterministic code-point-safe head/tail compression of working messages."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, replace
|
|
4
|
+
|
|
5
|
+
from ..config import CapConfig
|
|
6
|
+
from ..context import ContextCarrier, LayerDiagnostic
|
|
7
|
+
from ..errors import CapBudgetError, LayerStateError
|
|
8
|
+
from ..tokens import TokenCounter
|
|
9
|
+
from ..work import Cancellation, raise_if_cancelled
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def bounded_piece(
|
|
13
|
+
text: str,
|
|
14
|
+
allowance: int,
|
|
15
|
+
counter: TokenCounter,
|
|
16
|
+
*,
|
|
17
|
+
tail: bool = False,
|
|
18
|
+
cancellation: Cancellation | None = None,
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Find a fitting prefix/suffix; never assume BPE counts are strictly monotone."""
|
|
21
|
+
low, high = 0, len(text)
|
|
22
|
+
best = ""
|
|
23
|
+
while low <= high:
|
|
24
|
+
raise_if_cancelled(cancellation)
|
|
25
|
+
middle = (low + high) // 2
|
|
26
|
+
candidate = text[len(text) - middle :] if tail else text[:middle]
|
|
27
|
+
if counter.count_text(candidate) <= allowance:
|
|
28
|
+
best = candidate
|
|
29
|
+
low = middle + 1
|
|
30
|
+
else:
|
|
31
|
+
high = middle - 1
|
|
32
|
+
return best
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class CapLayer:
|
|
37
|
+
counter: TokenCounter
|
|
38
|
+
config: CapConfig = CapConfig()
|
|
39
|
+
|
|
40
|
+
def cap_text(self, text: str, *, cancellation: Cancellation | None = None) -> str:
|
|
41
|
+
raise_if_cancelled(cancellation)
|
|
42
|
+
if self.counter.count_text(text) <= self.config.max_tokens:
|
|
43
|
+
return text
|
|
44
|
+
head = bounded_piece(text, self.config.head_tokens, self.counter, cancellation=cancellation)
|
|
45
|
+
tail = bounded_piece(
|
|
46
|
+
text, self.config.tail_tokens, self.counter, tail=True, cancellation=cancellation
|
|
47
|
+
)
|
|
48
|
+
# At least one original code point at both ends is required for a capped message.
|
|
49
|
+
head, tail = head or text[:1], tail or text[-1:]
|
|
50
|
+
while head and tail:
|
|
51
|
+
raise_if_cancelled(cancellation)
|
|
52
|
+
end = len(text) - len(tail)
|
|
53
|
+
omitted = text[len(head) : end]
|
|
54
|
+
marker = f"\n[... {self.counter.count_text(omitted)} tokens elided ...]\n"
|
|
55
|
+
candidate = head + marker + tail
|
|
56
|
+
if omitted and self.counter.count_text(candidate) <= self.config.max_tokens:
|
|
57
|
+
return candidate
|
|
58
|
+
if len(head) >= len(tail) and len(head) > 1:
|
|
59
|
+
head = head[:-1]
|
|
60
|
+
elif len(tail) > 1:
|
|
61
|
+
tail = tail[1:]
|
|
62
|
+
else:
|
|
63
|
+
break
|
|
64
|
+
raise CapBudgetError(
|
|
65
|
+
"CAP allocation cannot fit a head, elision marker and tail",
|
|
66
|
+
max_tokens=self.config.max_tokens,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def apply(self, context: ContextCarrier) -> ContextCarrier:
|
|
70
|
+
if context.plan is not None:
|
|
71
|
+
raise LayerStateError("CAP must run before budget planning")
|
|
72
|
+
turns, changed = [], []
|
|
73
|
+
before = after = 0
|
|
74
|
+
for turn in context.original_turns:
|
|
75
|
+
raise_if_cancelled(context.cancellation)
|
|
76
|
+
messages = []
|
|
77
|
+
for message in turn.messages:
|
|
78
|
+
content = self.cap_text(message.content, cancellation=context.cancellation)
|
|
79
|
+
before += self.counter.count_text(message.content)
|
|
80
|
+
after += self.counter.count_text(content)
|
|
81
|
+
messages.append(replace(message, content=content))
|
|
82
|
+
working = replace(turn, messages=tuple(messages))
|
|
83
|
+
turns.append(working)
|
|
84
|
+
if working != turn:
|
|
85
|
+
changed.append(turn.turn_id)
|
|
86
|
+
return replace(
|
|
87
|
+
context,
|
|
88
|
+
working_turns=tuple(turns),
|
|
89
|
+
diagnostics=context.diagnostics
|
|
90
|
+
+ (
|
|
91
|
+
LayerDiagnostic(
|
|
92
|
+
"CAP",
|
|
93
|
+
"messages_capped" if changed else "unchanged",
|
|
94
|
+
tuple(changed),
|
|
95
|
+
before,
|
|
96
|
+
after,
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Shared rendering/accounting primitives used by layers and the public pipeline."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from dataclasses import asdict, replace
|
|
5
|
+
|
|
6
|
+
from ..context import ContextCarrier, LayerDiagnostic
|
|
7
|
+
from ..errors import LayerStateError
|
|
8
|
+
from ..models import (
|
|
9
|
+
BlockKind,
|
|
10
|
+
ChatRequest,
|
|
11
|
+
ContextBlock,
|
|
12
|
+
Message,
|
|
13
|
+
Role,
|
|
14
|
+
ToolDefinition,
|
|
15
|
+
Turn,
|
|
16
|
+
canonical_json,
|
|
17
|
+
)
|
|
18
|
+
from ..tokens import TokenCounter
|
|
19
|
+
from ..work import raise_if_cancelled
|
|
20
|
+
|
|
21
|
+
ORDER = tuple(BlockKind)
|
|
22
|
+
OPTIONAL = (BlockKind.SUMMARY, BlockKind.WINDOW, BlockKind.RETRIEVED)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def history_digest(turns: tuple[Turn, ...]) -> str:
|
|
26
|
+
values = [
|
|
27
|
+
(asdict(t.scope), t.turn_id, t.revision, t.timestamp.isoformat(), t.content_hash)
|
|
28
|
+
for t in turns
|
|
29
|
+
]
|
|
30
|
+
return hashlib.sha256(canonical_json(values).encode()).hexdigest()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def input_digest(context: ContextCarrier) -> str:
|
|
34
|
+
pins = []
|
|
35
|
+
for pin in context.pins.pins:
|
|
36
|
+
value = asdict(pin)
|
|
37
|
+
value["effective_at"] = pin.effective_at.isoformat()
|
|
38
|
+
value["expires_at"] = pin.expires_at.isoformat() if pin.expires_at else None
|
|
39
|
+
pins.append(value)
|
|
40
|
+
values = {
|
|
41
|
+
"scope": asdict(context.scope),
|
|
42
|
+
"original": history_digest(context.original_turns),
|
|
43
|
+
"working": history_digest(context.working_turns),
|
|
44
|
+
"pins": pins,
|
|
45
|
+
"question": context.question.to_wire(),
|
|
46
|
+
"question_id": context.question.message_id,
|
|
47
|
+
}
|
|
48
|
+
return hashlib.sha256(canonical_json(values).encode()).hexdigest()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def render_request(
|
|
52
|
+
blocks: tuple[ContextBlock, ...], question: Message, tools: tuple[ToolDefinition, ...] = ()
|
|
53
|
+
) -> ChatRequest:
|
|
54
|
+
"""Quote all historical/evidence blocks as user data, including historical tool calls."""
|
|
55
|
+
by_kind = {block.kind: block for block in blocks}
|
|
56
|
+
messages = []
|
|
57
|
+
for kind in ORDER:
|
|
58
|
+
if kind == BlockKind.QUESTION:
|
|
59
|
+
messages.append(Message("ctx:question", Role.USER, question.content))
|
|
60
|
+
continue
|
|
61
|
+
block = by_kind.get(kind)
|
|
62
|
+
if block is None or not block.content:
|
|
63
|
+
continue
|
|
64
|
+
role = Role.SYSTEM if kind == BlockKind.SYSTEM else Role.USER
|
|
65
|
+
content = (
|
|
66
|
+
block.content
|
|
67
|
+
if kind == BlockKind.SYSTEM
|
|
68
|
+
else f"[{kind.value.upper()} DATA]\n{block.content}"
|
|
69
|
+
)
|
|
70
|
+
messages.append(Message(f"ctx:{kind.value}", role, content))
|
|
71
|
+
return ChatRequest(tuple(messages), tools)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def required_blocks(context: ContextCarrier) -> tuple[ContextBlock, ...]:
|
|
75
|
+
return tuple(b for b in context.blocks if b.kind not in OPTIONAL)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def require_prepared(context: ContextCarrier, counter: TokenCounter) -> None:
|
|
79
|
+
if context.plan is None or context.required_request is None:
|
|
80
|
+
raise LayerStateError("PIN must reserve mandatory context first")
|
|
81
|
+
if context.prepared_fingerprint != input_digest(context):
|
|
82
|
+
raise LayerStateError("History, question or pins changed after budget planning; rerun PIN")
|
|
83
|
+
request = render_request(
|
|
84
|
+
required_blocks(context), context.question, context.required_request.tools
|
|
85
|
+
)
|
|
86
|
+
if (
|
|
87
|
+
request != context.required_request
|
|
88
|
+
or counter.count_request(request) != context.plan.required_estimate
|
|
89
|
+
):
|
|
90
|
+
raise LayerStateError("Mandatory blocks or counting profile changed after planning")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def set_block(
|
|
94
|
+
context: ContextCarrier, block: ContextBlock, diagnostic: LayerDiagnostic
|
|
95
|
+
) -> ContextCarrier:
|
|
96
|
+
blocks = tuple(b for b in context.blocks if b.kind != block.kind) + (block,)
|
|
97
|
+
return replace(
|
|
98
|
+
context,
|
|
99
|
+
blocks=tuple(sorted(blocks, key=lambda b: ORDER.index(b.kind))),
|
|
100
|
+
diagnostics=context.diagnostics + (diagnostic,),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def window_block(
|
|
105
|
+
context: ContextCarrier, ids: tuple[str, ...], counter: TokenCounter
|
|
106
|
+
) -> ContextBlock:
|
|
107
|
+
selected = [t for t in context.working_turns if t.turn_id in ids]
|
|
108
|
+
content = (
|
|
109
|
+
canonical_json(
|
|
110
|
+
[{"turn": t.turn_id, "messages": [m.to_wire() for m in t.messages]} for t in selected]
|
|
111
|
+
)
|
|
112
|
+
if selected
|
|
113
|
+
else ""
|
|
114
|
+
)
|
|
115
|
+
return ContextBlock(BlockKind.WINDOW, content, text_tokens=counter.count_text(content))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def incremental_cost(context: ContextCarrier, block: ContextBlock, counter: TokenCounter) -> int:
|
|
119
|
+
request = render_request(
|
|
120
|
+
required_blocks(context) + (block,), context.question, context.required_request.tools
|
|
121
|
+
)
|
|
122
|
+
return max(
|
|
123
|
+
0,
|
|
124
|
+
counter.count_request(request).estimated_tokens
|
|
125
|
+
- context.plan.required_estimate.estimated_tokens,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def fits_block(
|
|
130
|
+
context: ContextCarrier, block: ContextBlock, allowance: int, counter: TokenCounter
|
|
131
|
+
) -> bool:
|
|
132
|
+
if incremental_cost(context, block, counter) > allowance:
|
|
133
|
+
return False
|
|
134
|
+
blocks = tuple(b for b in context.blocks if b.kind != block.kind) + (block,)
|
|
135
|
+
# RETRIEVE runs first but must leave the exact planned WINDOW viable.
|
|
136
|
+
if context.window_planned and block.kind != BlockKind.WINDOW:
|
|
137
|
+
blocks = tuple(b for b in blocks if b.kind != BlockKind.WINDOW) + (
|
|
138
|
+
window_block(context, context.plan.window_turn_ids, counter),
|
|
139
|
+
)
|
|
140
|
+
request = render_request(blocks, context.question, context.required_request.tools)
|
|
141
|
+
return counter.count_request(request).estimated_tokens <= context.plan.input_allowance
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def plan_window(context: ContextCarrier, counter: TokenCounter) -> ContextCarrier:
|
|
145
|
+
require_prepared(context, counter)
|
|
146
|
+
selected: tuple[str, ...] = ()
|
|
147
|
+
for turn in reversed(context.working_turns):
|
|
148
|
+
raise_if_cancelled(context.cancellation)
|
|
149
|
+
candidate = (turn.turn_id,) + selected
|
|
150
|
+
if (
|
|
151
|
+
incremental_cost(context, window_block(context, candidate, counter), counter)
|
|
152
|
+
> context.plan.window_tokens
|
|
153
|
+
):
|
|
154
|
+
break
|
|
155
|
+
selected = candidate
|
|
156
|
+
if context.window_planned:
|
|
157
|
+
if selected != context.plan.window_turn_ids:
|
|
158
|
+
raise LayerStateError("Stored WINDOW membership differs from shared planner")
|
|
159
|
+
return context
|
|
160
|
+
return replace(
|
|
161
|
+
context, plan=replace(context.plan, window_turn_ids=selected), window_planned=True
|
|
162
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Reserve supplied instructions, active keyed facts, the question and tool schemas."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, replace
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from ..budget import plan_budget
|
|
7
|
+
from ..config import BudgetConfig
|
|
8
|
+
from ..context import ContextCarrier, LayerDiagnostic
|
|
9
|
+
from ..models import BlockKind, ContextBlock, ToolDefinition, aware_time, canonical_json, text_value
|
|
10
|
+
from ..tokens import TokenCounter
|
|
11
|
+
from .common import input_digest, render_request
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class PinLayer:
|
|
16
|
+
counter: TokenCounter
|
|
17
|
+
budget: BudgetConfig
|
|
18
|
+
system: str
|
|
19
|
+
at: datetime
|
|
20
|
+
tools: tuple[ToolDefinition, ...] = ()
|
|
21
|
+
|
|
22
|
+
def __post_init__(self) -> None:
|
|
23
|
+
text_value("system", self.system)
|
|
24
|
+
aware_time("at", self.at)
|
|
25
|
+
object.__setattr__(self, "tools", tuple(self.tools))
|
|
26
|
+
|
|
27
|
+
def apply(self, context: ContextCarrier) -> ContextCarrier:
|
|
28
|
+
active = tuple(pin for pin in context.pins.pins if pin.is_active(self.at))
|
|
29
|
+
content = (
|
|
30
|
+
canonical_json([{"key": pin.key, "value": pin.value} for pin in active])
|
|
31
|
+
if active
|
|
32
|
+
else ""
|
|
33
|
+
)
|
|
34
|
+
blocks = (
|
|
35
|
+
ContextBlock(
|
|
36
|
+
BlockKind.SYSTEM,
|
|
37
|
+
self.system,
|
|
38
|
+
prefix_stable=True,
|
|
39
|
+
text_tokens=self.counter.count_text(self.system),
|
|
40
|
+
),
|
|
41
|
+
ContextBlock(
|
|
42
|
+
BlockKind.PINNED,
|
|
43
|
+
content,
|
|
44
|
+
sources=tuple(pin.source for pin in active if pin.source is not None),
|
|
45
|
+
prefix_stable=True,
|
|
46
|
+
text_tokens=self.counter.count_text(content),
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
request = render_request(blocks, context.question, self.tools)
|
|
50
|
+
plan = plan_budget(required_request=request, config=self.budget, counter=self.counter)
|
|
51
|
+
# Re-running PIN deliberately invalidates all query-dependent layer outputs.
|
|
52
|
+
return replace(
|
|
53
|
+
context,
|
|
54
|
+
plan=plan,
|
|
55
|
+
blocks=blocks,
|
|
56
|
+
required_request=request,
|
|
57
|
+
prepared_fingerprint=input_digest(context),
|
|
58
|
+
window_planned=False,
|
|
59
|
+
retrieved_chunks=(),
|
|
60
|
+
diagnostics=context.diagnostics
|
|
61
|
+
+ (
|
|
62
|
+
LayerDiagnostic(
|
|
63
|
+
"PIN",
|
|
64
|
+
"required_context_reserved",
|
|
65
|
+
output_text_tokens=sum(b.text_tokens for b in blocks),
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""BM25+ over originals; optional recovery of missing chunks in capped WINDOW turns."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
import unicodedata
|
|
6
|
+
from dataclasses import asdict, dataclass, replace
|
|
7
|
+
|
|
8
|
+
from rank_bm25 import BM25Plus
|
|
9
|
+
|
|
10
|
+
from ..config import RetrievalConfig
|
|
11
|
+
from ..context import ContextCarrier, LayerDiagnostic
|
|
12
|
+
from ..errors import MemoryIntegrityError
|
|
13
|
+
from ..models import BlockKind, Chunk, ContextBlock, SourceRef, Turn, canonical_json
|
|
14
|
+
from ..tokens import TokenCounter
|
|
15
|
+
from ..work import Cancellation, raise_if_cancelled, validate_cancellation
|
|
16
|
+
from .common import fits_block, history_digest, plan_window, set_block
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ChunkIndex:
|
|
21
|
+
history_fingerprint: str
|
|
22
|
+
config_fingerprint: str
|
|
23
|
+
chunks: tuple[Chunk, ...]
|
|
24
|
+
|
|
25
|
+
def validate(self, history, config):
|
|
26
|
+
expected = hashlib.sha256(canonical_json(asdict(config)).encode()).hexdigest()
|
|
27
|
+
if (
|
|
28
|
+
self.history_fingerprint != history_digest(history)
|
|
29
|
+
or self.config_fingerprint != expected
|
|
30
|
+
):
|
|
31
|
+
raise MemoryIntegrityError("Chunk index history or configuration is stale")
|
|
32
|
+
originals = {t.turn_id: t for t in history}
|
|
33
|
+
if (
|
|
34
|
+
not isinstance(self.chunks, tuple)
|
|
35
|
+
or any(not isinstance(c, Chunk) for c in self.chunks)
|
|
36
|
+
or len({c.chunk_id for c in self.chunks}) != len(self.chunks)
|
|
37
|
+
):
|
|
38
|
+
raise MemoryIntegrityError("Invalid chunk index entries")
|
|
39
|
+
for chunk in self.chunks:
|
|
40
|
+
if not isinstance(chunk, Chunk) or chunk.source.turn_id not in originals:
|
|
41
|
+
raise MemoryIntegrityError("Chunk index contains unknown source")
|
|
42
|
+
chunk.validate_source(originals[chunk.source.turn_id])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def lexical_tokens(text: str) -> list[str]:
|
|
46
|
+
normalized = unicodedata.normalize("NFKC", text).casefold()
|
|
47
|
+
return re.findall(r"\w+(?:[-.]\w+)*", normalized)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def chunk_turns(
|
|
51
|
+
turns: tuple[Turn, ...], config: RetrievalConfig, *, cancellation: Cancellation | None = None
|
|
52
|
+
) -> tuple[Chunk, ...]:
|
|
53
|
+
validate_cancellation(cancellation)
|
|
54
|
+
raise_if_cancelled(cancellation)
|
|
55
|
+
chunks = []
|
|
56
|
+
version = f"characters-v1:{config.chunk_characters}:{config.overlap_characters}"
|
|
57
|
+
stride = config.chunk_characters - config.overlap_characters
|
|
58
|
+
for turn in turns:
|
|
59
|
+
raise_if_cancelled(cancellation)
|
|
60
|
+
for message in turn.messages:
|
|
61
|
+
raise_if_cancelled(cancellation)
|
|
62
|
+
message_hash = message.content_hash
|
|
63
|
+
for start in range(0, len(message.content), stride):
|
|
64
|
+
raise_if_cancelled(cancellation)
|
|
65
|
+
end = min(len(message.content), start + config.chunk_characters)
|
|
66
|
+
content = message.content[start:end]
|
|
67
|
+
if content.strip():
|
|
68
|
+
source = SourceRef(
|
|
69
|
+
turn.scope,
|
|
70
|
+
turn.turn_id,
|
|
71
|
+
message.message_id,
|
|
72
|
+
turn.revision,
|
|
73
|
+
start,
|
|
74
|
+
end,
|
|
75
|
+
message_hash,
|
|
76
|
+
)
|
|
77
|
+
identifier = hashlib.sha256(
|
|
78
|
+
canonical_json([asdict(source), version]).encode()
|
|
79
|
+
).hexdigest()
|
|
80
|
+
chunks.append(Chunk(identifier, source, content, version))
|
|
81
|
+
if end == len(message.content):
|
|
82
|
+
break
|
|
83
|
+
return tuple(chunks)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def evidence_block(chunks: tuple[Chunk, ...], counter: TokenCounter) -> ContextBlock:
|
|
87
|
+
content = (
|
|
88
|
+
canonical_json(
|
|
89
|
+
[
|
|
90
|
+
{
|
|
91
|
+
"turn": c.source.turn_id,
|
|
92
|
+
"message": c.source.message_id,
|
|
93
|
+
"start": c.source.start,
|
|
94
|
+
"end": c.source.end,
|
|
95
|
+
"text": c.content,
|
|
96
|
+
}
|
|
97
|
+
for c in chunks
|
|
98
|
+
]
|
|
99
|
+
)
|
|
100
|
+
if chunks
|
|
101
|
+
else ""
|
|
102
|
+
)
|
|
103
|
+
return ContextBlock(
|
|
104
|
+
BlockKind.RETRIEVED,
|
|
105
|
+
content,
|
|
106
|
+
sources=tuple(c.source for c in chunks),
|
|
107
|
+
text_tokens=counter.count_text(content),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def overlaps(left: Chunk, right: Chunk) -> bool:
|
|
112
|
+
a, b = left.source, right.source
|
|
113
|
+
return (a.scope, a.turn_id, a.message_id, a.revision) == (
|
|
114
|
+
b.scope,
|
|
115
|
+
b.turn_id,
|
|
116
|
+
b.message_id,
|
|
117
|
+
b.revision,
|
|
118
|
+
) and max(a.start, b.start) < min(a.end, b.end)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def window_messages(context: ContextCarrier) -> dict[tuple[str, str], str]:
|
|
122
|
+
"""Actual working content, not full-original provenance ranges on WINDOW blocks."""
|
|
123
|
+
return {
|
|
124
|
+
(turn.turn_id, message.message_id): message.content
|
|
125
|
+
for turn in context.working_turns
|
|
126
|
+
if turn.turn_id in context.plan.window_turn_ids
|
|
127
|
+
for message in turn.messages
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def represented_in_window(chunk: Chunk, visible: dict[tuple[str, str], str]) -> bool:
|
|
132
|
+
text = visible.get((chunk.source.turn_id, chunk.source.message_id))
|
|
133
|
+
return text is not None and chunk.content in text
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True)
|
|
137
|
+
class RetrieveLayer:
|
|
138
|
+
counter: TokenCounter
|
|
139
|
+
config: RetrievalConfig = RetrievalConfig()
|
|
140
|
+
chunk_index: ChunkIndex | None = None
|
|
141
|
+
|
|
142
|
+
def apply(self, context: ContextCarrier) -> ContextCarrier:
|
|
143
|
+
if self.chunk_index is not None:
|
|
144
|
+
if not isinstance(self.chunk_index, ChunkIndex):
|
|
145
|
+
raise MemoryIntegrityError("Expected a validated chunk index")
|
|
146
|
+
self.chunk_index.validate(context.original_turns, self.config)
|
|
147
|
+
planned = plan_window(context, self.counter)
|
|
148
|
+
visible = window_messages(planned)
|
|
149
|
+
old = tuple(
|
|
150
|
+
t
|
|
151
|
+
for t in planned.original_turns
|
|
152
|
+
if t.turn_id not in planned.plan.window_turn_ids
|
|
153
|
+
or (
|
|
154
|
+
self.config.recover_capped_window
|
|
155
|
+
and any(m.content != visible[(t.turn_id, m.message_id)] for m in t.messages)
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
query = sorted(set(lexical_tokens(planned.question.content)))
|
|
159
|
+
selected: tuple[Chunk, ...] = ()
|
|
160
|
+
reason = "no_match"
|
|
161
|
+
if not query:
|
|
162
|
+
reason = "empty_query"
|
|
163
|
+
elif not old:
|
|
164
|
+
reason = "no_old_history"
|
|
165
|
+
elif planned.plan.retrieval_tokens == 0:
|
|
166
|
+
reason = "zero_reserve"
|
|
167
|
+
else:
|
|
168
|
+
if self.chunk_index is None:
|
|
169
|
+
# Preserve the exact 0.8.0 call form when no token is supplied.
|
|
170
|
+
chunks = (
|
|
171
|
+
chunk_turns(old, self.config)
|
|
172
|
+
if context.cancellation is None
|
|
173
|
+
else chunk_turns(old, self.config, cancellation=context.cancellation)
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
ids = {t.turn_id for t in old}
|
|
177
|
+
chunks = tuple(c for c in self.chunk_index.chunks if c.source.turn_id in ids)
|
|
178
|
+
searchable = []
|
|
179
|
+
for chunk in chunks:
|
|
180
|
+
raise_if_cancelled(context.cancellation)
|
|
181
|
+
if represented_in_window(chunk, visible):
|
|
182
|
+
continue
|
|
183
|
+
terms = lexical_tokens(chunk.content)
|
|
184
|
+
if terms:
|
|
185
|
+
searchable.append((chunk, terms))
|
|
186
|
+
if searchable:
|
|
187
|
+
# BM25+ has positive IDF for one-document/small corpora. Its delta
|
|
188
|
+
# also scores nonmatches: explicitly require lexical intersection.
|
|
189
|
+
ranker = BM25Plus(
|
|
190
|
+
[terms for _, terms in searchable],
|
|
191
|
+
k1=self.config.k1,
|
|
192
|
+
b=self.config.b,
|
|
193
|
+
delta=1.0,
|
|
194
|
+
)
|
|
195
|
+
scores = ranker.get_scores(query)
|
|
196
|
+
raise_if_cancelled(context.cancellation)
|
|
197
|
+
ranked = [
|
|
198
|
+
(float(score), c)
|
|
199
|
+
for (c, terms), score in zip(searchable, scores, strict=True)
|
|
200
|
+
if set(query).intersection(terms) and score > self.config.minimum_score
|
|
201
|
+
]
|
|
202
|
+
ranked.sort(key=lambda item: (-item[0], item[1].chunk_id))
|
|
203
|
+
raise_if_cancelled(context.cancellation)
|
|
204
|
+
if ranked:
|
|
205
|
+
reason = "evidence_over_budget"
|
|
206
|
+
for _, chunk in ranked:
|
|
207
|
+
raise_if_cancelled(context.cancellation)
|
|
208
|
+
if any(overlaps(chunk, existing) for existing in selected):
|
|
209
|
+
continue
|
|
210
|
+
candidate = selected + (chunk,)
|
|
211
|
+
if fits_block(
|
|
212
|
+
planned,
|
|
213
|
+
evidence_block(candidate, self.counter),
|
|
214
|
+
planned.plan.retrieval_tokens,
|
|
215
|
+
self.counter,
|
|
216
|
+
):
|
|
217
|
+
selected = candidate
|
|
218
|
+
if len(selected) >= self.config.top_k:
|
|
219
|
+
break
|
|
220
|
+
if selected:
|
|
221
|
+
reason = "evidence_retrieved"
|
|
222
|
+
block = evidence_block(selected, self.counter)
|
|
223
|
+
result = set_block(
|
|
224
|
+
planned,
|
|
225
|
+
block,
|
|
226
|
+
LayerDiagnostic(
|
|
227
|
+
"RETRIEVE",
|
|
228
|
+
reason,
|
|
229
|
+
tuple(dict.fromkeys(c.source.turn_id for c in selected)),
|
|
230
|
+
output_text_tokens=block.text_tokens,
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
return replace(result, retrieved_chunks=selected)
|