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,15 @@
|
|
|
1
|
+
"""Core contracts. Importing this package performs no configuration or network I/O."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.12.0"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def __getattr__(name):
|
|
7
|
+
"""Expose the real typed API lazily, without loading tokenizers on package import."""
|
|
8
|
+
if name in ("assemble_context", "AssembledContext", "AssemblyOptions"):
|
|
9
|
+
from . import pipeline
|
|
10
|
+
|
|
11
|
+
return getattr(pipeline, name)
|
|
12
|
+
raise AttributeError(name)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ["assemble_context", "AssembledContext", "AssemblyOptions", "__version__"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""SQLite connection policy shared by local stores, without provider dependencies."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sqlite3
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def connect(*args, **kwargs):
|
|
8
|
+
db = sqlite3.connect(*args, **kwargs)
|
|
9
|
+
try:
|
|
10
|
+
if os.name == "nt":
|
|
11
|
+
# Sort/index temporary data must not spill into an unchecked OS temp
|
|
12
|
+
# directory. Durable rollback/WAL files still use the private DB dir.
|
|
13
|
+
db.execute("PRAGMA temp_store=MEMORY")
|
|
14
|
+
if db.execute("PRAGMA temp_store").fetchone()[0] != 2:
|
|
15
|
+
raise sqlite3.OperationalError("Private SQLite temporary storage unavailable")
|
|
16
|
+
return db
|
|
17
|
+
except BaseException:
|
|
18
|
+
db.close()
|
|
19
|
+
raise
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Fail-closed Windows credential reads; never change an operator's ACL.
|
|
2
|
+
|
|
3
|
+
Parent directories and local administrators are trusted, as on POSIX. Only the
|
|
4
|
+
effective user, LocalSystem and built-in Administrators may own the file or have
|
|
5
|
+
applicable allow ACEs. Complex/unknown ACE forms are rejected, not approximated.
|
|
6
|
+
Native imports are lazy so this module is harmless on non-Windows platforms.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .errors import ContractError
|
|
12
|
+
|
|
13
|
+
TRUSTED_SYSTEM_SIDS = frozenset({"S-1-5-18", "S-1-5-32-544"})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_acl(owner, user, entries):
|
|
17
|
+
"""Evaluate normalized (type, flags, mask, SID) ACEs, including inherited ACEs."""
|
|
18
|
+
trusted = TRUSTED_SYSTEM_SIDS | {user}
|
|
19
|
+
if not user or owner not in trusted or entries is None:
|
|
20
|
+
raise ContractError("Configuration must have a private Windows ACL")
|
|
21
|
+
for kind, flags, mask, sid in entries:
|
|
22
|
+
if flags & 0x08: # INHERIT_ONLY_ACE does not apply to the file itself.
|
|
23
|
+
continue
|
|
24
|
+
if kind not in (0, 1): # Standard allow/deny only; fail closed on complex ACEs.
|
|
25
|
+
raise ContractError("Unsupported Windows configuration ACL")
|
|
26
|
+
# A deny ACE never excuses a broad allow. This intentionally conservative
|
|
27
|
+
# policy avoids pretending to implement Windows' effective-access algorithm.
|
|
28
|
+
if kind == 0 and mask and sid not in trusted:
|
|
29
|
+
raise ContractError("Configuration must have a private Windows ACL")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WindowsReader:
|
|
33
|
+
def __init__(self):
|
|
34
|
+
import pywintypes
|
|
35
|
+
import win32api
|
|
36
|
+
import win32con
|
|
37
|
+
import win32file
|
|
38
|
+
import win32security
|
|
39
|
+
|
|
40
|
+
self.error = pywintypes.error
|
|
41
|
+
self.api, self.con = win32api, win32con
|
|
42
|
+
self.file, self.security = win32file, win32security
|
|
43
|
+
|
|
44
|
+
def local_path(self, path):
|
|
45
|
+
path = Path(path).absolute()
|
|
46
|
+
# Exclude device/UNC namespaces, alternate streams and remote drives before
|
|
47
|
+
# CreateFile can open a pipe/device or initiate a remote connection.
|
|
48
|
+
if (
|
|
49
|
+
len(path.drive) != 2
|
|
50
|
+
or path.drive[1] != ":"
|
|
51
|
+
or not path.drive[0].isascii()
|
|
52
|
+
or not path.drive[0].isalpha()
|
|
53
|
+
or path.is_reserved()
|
|
54
|
+
or any(
|
|
55
|
+
any(char in part for char in ':<>"|?*') or part.endswith((".", " "))
|
|
56
|
+
for part in path.parts[1:]
|
|
57
|
+
)
|
|
58
|
+
or self.file.GetDriveType(path.anchor) != 3 # DRIVE_FIXED
|
|
59
|
+
):
|
|
60
|
+
raise ContractError("Configuration requires a local disk file")
|
|
61
|
+
return path
|
|
62
|
+
|
|
63
|
+
def open(self, path):
|
|
64
|
+
c = self.con
|
|
65
|
+
path = self.local_path(path)
|
|
66
|
+
return self.file.CreateFile(
|
|
67
|
+
str(path),
|
|
68
|
+
c.GENERIC_READ | c.READ_CONTROL,
|
|
69
|
+
c.FILE_SHARE_READ, # No concurrent writer or delete/rename handle.
|
|
70
|
+
None, # Non-inheritable handle; no ACL mutation.
|
|
71
|
+
c.OPEN_EXISTING,
|
|
72
|
+
self.file.FILE_FLAG_OPEN_REPARSE_POINT | c.FILE_FLAG_BACKUP_SEMANTICS,
|
|
73
|
+
None,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def user(self):
|
|
77
|
+
s, c = self.security, self.con
|
|
78
|
+
try:
|
|
79
|
+
token = s.OpenThreadToken(self.api.GetCurrentThread(), c.TOKEN_QUERY, True)
|
|
80
|
+
except self.error as exc:
|
|
81
|
+
if exc.winerror != 1008: # ERROR_NO_TOKEN only; other errors fail closed.
|
|
82
|
+
raise
|
|
83
|
+
token = s.OpenProcessToken(self.api.GetCurrentProcess(), c.TOKEN_QUERY)
|
|
84
|
+
try:
|
|
85
|
+
return s.ConvertSidToStringSid(s.GetTokenInformation(token, s.TokenUser)[0])
|
|
86
|
+
finally:
|
|
87
|
+
token.Close()
|
|
88
|
+
|
|
89
|
+
def validate(self, handle, *, directory=False):
|
|
90
|
+
# Validate the opened object, not a path that can be replaced before reading.
|
|
91
|
+
attributes = self.file.GetFileInformationByHandle(handle)[0]
|
|
92
|
+
if (
|
|
93
|
+
self.file.GetFileType(handle) != 1
|
|
94
|
+
or attributes & 0x400
|
|
95
|
+
or bool(attributes & 0x10) != directory
|
|
96
|
+
):
|
|
97
|
+
raise ContractError("Configuration must be a regular non-reparse file")
|
|
98
|
+
s = self.security
|
|
99
|
+
sd = s.GetSecurityInfo(
|
|
100
|
+
handle, s.SE_FILE_OBJECT, s.OWNER_SECURITY_INFORMATION | s.DACL_SECURITY_INFORMATION
|
|
101
|
+
)
|
|
102
|
+
owner = s.ConvertSidToStringSid(sd.GetSecurityDescriptorOwner())
|
|
103
|
+
acl = sd.GetSecurityDescriptorDacl()
|
|
104
|
+
entries = None
|
|
105
|
+
if acl is not None:
|
|
106
|
+
entries = []
|
|
107
|
+
for index in range(acl.GetAceCount()):
|
|
108
|
+
ace = acl.GetAce(index)
|
|
109
|
+
kind, flags = ace[0]
|
|
110
|
+
# Object/callback ACE layouts differ; do not misinterpret their SID.
|
|
111
|
+
sid = s.ConvertSidToStringSid(ace[2]) if kind in (0, 1) else None
|
|
112
|
+
entries.append((kind, flags, ace[1], sid))
|
|
113
|
+
user = self.user()
|
|
114
|
+
check_acl(owner, user, entries)
|
|
115
|
+
if directory:
|
|
116
|
+
# Include inherit-only grants: SQLite-created journals inherit them.
|
|
117
|
+
check_acl(owner, user, [(k, f & ~0x08, m, s) for k, f, m, s in entries])
|
|
118
|
+
if not any(
|
|
119
|
+
k == 0 and f & 0x01 and s == user and m & 0x1F01FF == 0x1F01FF
|
|
120
|
+
for k, f, m, s in entries
|
|
121
|
+
):
|
|
122
|
+
raise ContractError("Storage directory requires private file inheritance")
|
|
123
|
+
|
|
124
|
+
def read(self, handle, size):
|
|
125
|
+
return self.file.ReadFile(handle, size)[1]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Private local SQLite files and inheritable directory ACLs on Windows.
|
|
2
|
+
|
|
3
|
+
SQLite reopens paths, so ancestors, the effective user, SYSTEM and administrators
|
|
4
|
+
must be trusted. This is not a sandbox against those principals. Existing ACLs
|
|
5
|
+
are never rewritten. All native imports are lazy; no unchecked fallback exists.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from glob import escape
|
|
9
|
+
|
|
10
|
+
from ._windows_security import TRUSTED_SYSTEM_SIDS, WindowsReader
|
|
11
|
+
from .errors import StorageError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WindowsStorage(WindowsReader):
|
|
15
|
+
def attributes(self):
|
|
16
|
+
import pywintypes
|
|
17
|
+
|
|
18
|
+
s = self.security
|
|
19
|
+
acl = s.ACL()
|
|
20
|
+
for sid in sorted(TRUSTED_SYSTEM_SIDS | {self.user()}):
|
|
21
|
+
acl.AddAccessAllowedAceEx(
|
|
22
|
+
s.ACL_REVISION, 0x01 | 0x02, 0x1F01FF, s.ConvertStringSidToSid(sid)
|
|
23
|
+
)
|
|
24
|
+
sd = s.SECURITY_DESCRIPTOR()
|
|
25
|
+
sd.SetSecurityDescriptorDacl(True, acl, False)
|
|
26
|
+
sd.SetSecurityDescriptorOwner(s.ConvertStringSidToSid(self.user()), False)
|
|
27
|
+
sd.SetSecurityDescriptorControl(s.SE_DACL_PROTECTED, s.SE_DACL_PROTECTED)
|
|
28
|
+
attributes = pywintypes.SECURITY_ATTRIBUTES()
|
|
29
|
+
attributes.SECURITY_DESCRIPTOR = sd
|
|
30
|
+
attributes.bInheritHandle = False
|
|
31
|
+
return attributes
|
|
32
|
+
|
|
33
|
+
def open_storage(self, path, *, create=False, exclusive=False):
|
|
34
|
+
c = self.con
|
|
35
|
+
|
|
36
|
+
def open_with(disposition, attributes):
|
|
37
|
+
return self.file.CreateFile(
|
|
38
|
+
str(path),
|
|
39
|
+
c.READ_CONTROL,
|
|
40
|
+
c.FILE_SHARE_READ | c.FILE_SHARE_WRITE | c.FILE_SHARE_DELETE,
|
|
41
|
+
attributes,
|
|
42
|
+
disposition,
|
|
43
|
+
self.file.FILE_FLAG_OPEN_REPARSE_POINT | c.FILE_FLAG_BACKUP_SEMANTICS,
|
|
44
|
+
None,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if create:
|
|
48
|
+
try:
|
|
49
|
+
return open_with(c.CREATE_NEW, self.attributes())
|
|
50
|
+
except self.error as exc:
|
|
51
|
+
if exclusive or exc.winerror not in (80, 183):
|
|
52
|
+
raise
|
|
53
|
+
return open_with(c.OPEN_EXISTING, None)
|
|
54
|
+
|
|
55
|
+
def directory(self, path, *, create):
|
|
56
|
+
try:
|
|
57
|
+
handle = self.open_storage(path)
|
|
58
|
+
except self.error as exc:
|
|
59
|
+
if not create or exc.winerror not in (2, 3) or path == path.parent:
|
|
60
|
+
raise
|
|
61
|
+
# Only missing directories get new ACLs. Ancestors remain trusted.
|
|
62
|
+
self.make_directory(path)
|
|
63
|
+
handle = self.open_storage(path)
|
|
64
|
+
try:
|
|
65
|
+
self.validate(handle, directory=True)
|
|
66
|
+
finally:
|
|
67
|
+
handle.Close()
|
|
68
|
+
|
|
69
|
+
def make_directory(self, path):
|
|
70
|
+
# Security is applied at creation, never via a later permission rewrite.
|
|
71
|
+
if not path.parent.exists():
|
|
72
|
+
self.make_directory(path.parent)
|
|
73
|
+
try:
|
|
74
|
+
self.file.CreateDirectory(str(path), self.attributes())
|
|
75
|
+
except self.error as exc:
|
|
76
|
+
if exc.winerror != 183: # Concurrent creator: validate below.
|
|
77
|
+
raise
|
|
78
|
+
|
|
79
|
+
def file_boundary(self, path, *, create=False, exclusive=False, optional=False):
|
|
80
|
+
try:
|
|
81
|
+
handle = self.open_storage(path, create=create, exclusive=exclusive)
|
|
82
|
+
except self.error as exc:
|
|
83
|
+
if optional and exc.winerror == 2:
|
|
84
|
+
return
|
|
85
|
+
raise
|
|
86
|
+
try:
|
|
87
|
+
self.validate(handle)
|
|
88
|
+
if self.file.GetFileInformationByHandle(handle)[7] != 1:
|
|
89
|
+
raise StorageError("Private storage cannot have hard-link aliases")
|
|
90
|
+
finally:
|
|
91
|
+
handle.Close()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def private_file(path, *, create=False, exclusive=False):
|
|
95
|
+
"""Validate the DB, parent inheritance and any existing SQLite sidecars."""
|
|
96
|
+
try:
|
|
97
|
+
boundary = WindowsStorage()
|
|
98
|
+
path = boundary.local_path(path)
|
|
99
|
+
boundary.directory(path.parent, create=create)
|
|
100
|
+
boundary.file_boundary(path, create=create, exclusive=exclusive)
|
|
101
|
+
for suffix in ("-journal", "-wal", "-shm"):
|
|
102
|
+
boundary.file_boundary(path.with_name(path.name + suffix), optional=True)
|
|
103
|
+
# Multi-database DELETE transactions can leave super-journals.
|
|
104
|
+
for sidecar in path.parent.glob(escape(path.name) + "-mj*"):
|
|
105
|
+
boundary.file_boundary(sidecar, optional=True)
|
|
106
|
+
return path
|
|
107
|
+
except Exception:
|
|
108
|
+
raise StorageError(
|
|
109
|
+
"Storage requires private local Windows files and directory ACLs"
|
|
110
|
+
) from None
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Provider and framework message adapters. Optional; no provider SDK is imported.
|
|
2
|
+
|
|
3
|
+
Assembly output is one system message, user data blocks and the user question, plus
|
|
4
|
+
optional tool schemas. These helpers reshape it for common clients:
|
|
5
|
+
|
|
6
|
+
client.chat.completions.create(model=..., **openai_chat(result))
|
|
7
|
+
client.messages.create(model=..., max_tokens=..., **anthropic_messages(result))
|
|
8
|
+
chat_model.invoke(langchain_messages(result)) # needs the [langchain] extra
|
|
9
|
+
|
|
10
|
+
`turns_from_openai` goes the other way: it converts an existing OpenAI-style chat
|
|
11
|
+
history into `Turn`s for `assemble_context` or `MemoryStore`.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from collections.abc import Iterable, Mapping
|
|
16
|
+
from datetime import UTC, datetime
|
|
17
|
+
|
|
18
|
+
from .errors import ContractError
|
|
19
|
+
from .models import ChatRequest, Message, Role, Scope, ToolCall, Turn
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _request(result) -> ChatRequest:
|
|
23
|
+
request = getattr(result, "request", result)
|
|
24
|
+
if not isinstance(request, ChatRequest):
|
|
25
|
+
raise ContractError("Expected an AssembledContext or ChatRequest")
|
|
26
|
+
return request
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def openai_chat(result) -> dict:
|
|
30
|
+
"""`messages` (and `tools`, when present) for OpenAI-compatible chat APIs."""
|
|
31
|
+
return _request(result).to_wire()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def anthropic_messages(result) -> dict:
|
|
35
|
+
"""`system`, `messages` (and `tools`) for the Anthropic Messages API.
|
|
36
|
+
|
|
37
|
+
The system prompt becomes the top-level `system` field; consecutive user data
|
|
38
|
+
blocks are merged into one user turn of text blocks, as the API expects.
|
|
39
|
+
"""
|
|
40
|
+
request = _request(result)
|
|
41
|
+
system, messages = [], []
|
|
42
|
+
for message in request.messages:
|
|
43
|
+
if message.role == Role.SYSTEM:
|
|
44
|
+
system.append(message.content)
|
|
45
|
+
elif message.role == Role.USER and not message.tool_calls:
|
|
46
|
+
block = {"type": "text", "text": message.content}
|
|
47
|
+
if messages and messages[-1]["role"] == "user":
|
|
48
|
+
messages[-1]["content"].append(block)
|
|
49
|
+
else:
|
|
50
|
+
messages.append({"role": "user", "content": [block]})
|
|
51
|
+
else:
|
|
52
|
+
raise ContractError("Anthropic adapter supports assembled system/user messages only")
|
|
53
|
+
payload: dict = {"system": "\n\n".join(system), "messages": messages}
|
|
54
|
+
if request.tools:
|
|
55
|
+
payload["tools"] = [
|
|
56
|
+
{
|
|
57
|
+
"name": tool.name,
|
|
58
|
+
"description": tool.description,
|
|
59
|
+
"input_schema": json.loads(tool.parameters_json),
|
|
60
|
+
}
|
|
61
|
+
for tool in request.tools
|
|
62
|
+
]
|
|
63
|
+
return payload
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def langchain_messages(result) -> list:
|
|
67
|
+
"""LangChain `SystemMessage`/`HumanMessage` objects; needs `langchain-core`."""
|
|
68
|
+
try:
|
|
69
|
+
from langchain_core.messages import HumanMessage, SystemMessage
|
|
70
|
+
except ImportError:
|
|
71
|
+
raise ImportError(
|
|
72
|
+
"langchain_messages needs langchain-core: "
|
|
73
|
+
"pip install 'context-engineering-core[langchain]'"
|
|
74
|
+
) from None
|
|
75
|
+
converted = []
|
|
76
|
+
for message in _request(result).messages:
|
|
77
|
+
if message.role == Role.SYSTEM:
|
|
78
|
+
converted.append(SystemMessage(content=message.content))
|
|
79
|
+
elif message.role == Role.USER and not message.tool_calls:
|
|
80
|
+
converted.append(HumanMessage(content=message.content))
|
|
81
|
+
else:
|
|
82
|
+
raise ContractError("LangChain adapter supports assembled system/user messages only")
|
|
83
|
+
return converted
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _text(content, index: int) -> str:
|
|
87
|
+
if content is None:
|
|
88
|
+
return ""
|
|
89
|
+
if isinstance(content, str):
|
|
90
|
+
return content
|
|
91
|
+
if isinstance(content, list) and all(
|
|
92
|
+
isinstance(part, Mapping) and part.get("type") == "text" for part in content
|
|
93
|
+
):
|
|
94
|
+
return "".join(str(part.get("text", "")) for part in content)
|
|
95
|
+
raise ContractError("Only text message content is supported", field=f"messages[{index}]")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def turns_from_openai(
|
|
99
|
+
messages: Iterable[Mapping],
|
|
100
|
+
scope: Scope,
|
|
101
|
+
*,
|
|
102
|
+
id_prefix: str = "",
|
|
103
|
+
timestamp: datetime | None = None,
|
|
104
|
+
) -> tuple[Turn, ...]:
|
|
105
|
+
"""Group an OpenAI-style history into turns: each user message starts a new turn.
|
|
106
|
+
|
|
107
|
+
IDs come from positions (`{id_prefix}t{n}` / `{id_prefix}m{i}`), so converting
|
|
108
|
+
the same history twice yields the same IDs and provenance. Pass the system prompt
|
|
109
|
+
to `assemble_context(system=...)` instead of including it here.
|
|
110
|
+
"""
|
|
111
|
+
if not isinstance(scope, Scope):
|
|
112
|
+
raise ContractError("turns_from_openai requires a Scope")
|
|
113
|
+
at = timestamp or datetime.now(UTC)
|
|
114
|
+
groups: list[list[Message]] = []
|
|
115
|
+
for index, item in enumerate(messages):
|
|
116
|
+
if not isinstance(item, Mapping):
|
|
117
|
+
raise ContractError("Each message must be a mapping", field=f"messages[{index}]")
|
|
118
|
+
role = item.get("role")
|
|
119
|
+
if role == "system" or role == "developer":
|
|
120
|
+
raise ContractError(
|
|
121
|
+
"Pass system instructions via system=, not history", field=f"messages[{index}]"
|
|
122
|
+
)
|
|
123
|
+
if role not in ("user", "assistant", "tool"):
|
|
124
|
+
raise ContractError("Unsupported message role", field=f"messages[{index}]")
|
|
125
|
+
try:
|
|
126
|
+
calls = tuple(
|
|
127
|
+
ToolCall(call["id"], call["function"]["name"], call["function"]["arguments"])
|
|
128
|
+
for call in item.get("tool_calls") or ()
|
|
129
|
+
)
|
|
130
|
+
except (KeyError, TypeError):
|
|
131
|
+
raise ContractError("Malformed tool call", field=f"messages[{index}]") from None
|
|
132
|
+
message = Message(
|
|
133
|
+
f"{id_prefix}m{index}",
|
|
134
|
+
Role(role),
|
|
135
|
+
_text(item.get("content"), index),
|
|
136
|
+
tool_call_id=item.get("tool_call_id"),
|
|
137
|
+
tool_calls=calls,
|
|
138
|
+
)
|
|
139
|
+
if role == "user" or not groups:
|
|
140
|
+
groups.append([])
|
|
141
|
+
groups[-1].append(message)
|
|
142
|
+
return tuple(
|
|
143
|
+
Turn(f"{id_prefix}t{number}", scope, tuple(group), timestamp=at)
|
|
144
|
+
for number, group in enumerate(groups)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
__all__ = ["anthropic_messages", "langchain_messages", "openai_chat", "turns_from_openai"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Opt-in exact JSON answer conformance; never truth grading or inference."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import unicodedata
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from .errors import ContractError
|
|
9
|
+
|
|
10
|
+
MAX_DOCUMENT_BYTES = 16384
|
|
11
|
+
PATTERNS = {
|
|
12
|
+
"ascii_identifier": r"[A-Za-z0-9][A-Za-z0-9._:/-]*",
|
|
13
|
+
"integer": r"(?:0|-?[1-9][0-9]*|UNKNOWN)",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class AnswerContract:
|
|
19
|
+
"""Strict value preservation. The caller owns evidence and authorization checks."""
|
|
20
|
+
|
|
21
|
+
kind: str = "text"
|
|
22
|
+
max_characters: int = 512
|
|
23
|
+
|
|
24
|
+
def __post_init__(self):
|
|
25
|
+
if self.kind not in ("text", "ascii_identifier", "integer"):
|
|
26
|
+
raise ContractError("Unsupported answer kind")
|
|
27
|
+
if type(self.max_characters) is not int or not 7 <= self.max_characters <= 2048:
|
|
28
|
+
raise ContractError("Answer length limit must be between7 and2048")
|
|
29
|
+
|
|
30
|
+
def json_schema(self) -> dict:
|
|
31
|
+
value = {"type": "string", "minLength": 1, "maxLength": self.max_characters}
|
|
32
|
+
if self.kind in PATTERNS:
|
|
33
|
+
value["pattern"] = "^(?:" + PATTERNS[self.kind] + ")$"
|
|
34
|
+
return {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": False,
|
|
37
|
+
"required": ["answer"],
|
|
38
|
+
"properties": {"answer": value},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def instructions(self) -> str:
|
|
42
|
+
constraint = {
|
|
43
|
+
"text": "Preserve the exact value, including case and Unicode characters.",
|
|
44
|
+
"ascii_identifier": "Use an exact ASCII identifier; never replace its hyphens or case.",
|
|
45
|
+
"integer": "Use a base-ten integer string without plus signs or leading zeros.",
|
|
46
|
+
}[self.kind]
|
|
47
|
+
return (
|
|
48
|
+
'Return only one JSON object with exactly one string field named "answer". '
|
|
49
|
+
'If evidence is missing or unresolved, return {"answer":"UNKNOWN"}. '
|
|
50
|
+
"Do not include Markdown, explanations, extra fields, or surrounding value whitespace. "
|
|
51
|
+
"The value must be a single line with no control characters. "
|
|
52
|
+
f"Maximum value length: {self.max_characters} characters. " + constraint
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def parse(self, document: str) -> str:
|
|
56
|
+
"""Reject, never repair/extract/normalize. Sanitized errors omit response bodies."""
|
|
57
|
+
try:
|
|
58
|
+
if not isinstance(document, str) or len(document) > MAX_DOCUMENT_BYTES:
|
|
59
|
+
raise ValueError
|
|
60
|
+
if len(document.encode("utf-8")) > MAX_DOCUMENT_BYTES:
|
|
61
|
+
raise ValueError
|
|
62
|
+
|
|
63
|
+
def pairs(items):
|
|
64
|
+
result = {}
|
|
65
|
+
for key, value in items:
|
|
66
|
+
if key in result:
|
|
67
|
+
raise ValueError
|
|
68
|
+
result[key] = value
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
def invalid_constant(_value):
|
|
72
|
+
raise ValueError
|
|
73
|
+
|
|
74
|
+
value = json.loads(document, object_pairs_hook=pairs, parse_constant=invalid_constant)
|
|
75
|
+
if not isinstance(value, dict) or set(value) != {"answer"}:
|
|
76
|
+
raise ValueError
|
|
77
|
+
answer = value["answer"]
|
|
78
|
+
if (
|
|
79
|
+
not isinstance(answer, str)
|
|
80
|
+
or not 1 <= len(answer) <= self.max_characters
|
|
81
|
+
or answer != answer.strip()
|
|
82
|
+
or any(unicodedata.category(c) in {"Cc", "Cs", "Zl", "Zp"} for c in answer)
|
|
83
|
+
):
|
|
84
|
+
raise ValueError
|
|
85
|
+
answer.encode("utf-8")
|
|
86
|
+
if self.kind in PATTERNS and not re.fullmatch(PATTERNS[self.kind], answer):
|
|
87
|
+
raise ValueError
|
|
88
|
+
return answer
|
|
89
|
+
except (ValueError, TypeError, UnicodeError, RecursionError):
|
|
90
|
+
raise ContractError("Answer does not conform to the exact JSON contract") from None
|
context_engine/budget.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Mandatory budget reservation and complete serialized-request admission."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .config import BudgetConfig
|
|
6
|
+
from .errors import ContractError, LayerAllocationError, PromptTooLarge, RequiredContextTooLarge
|
|
7
|
+
from .models import ChatRequest, freeze_sequence, integer_value
|
|
8
|
+
from .tokens import TokenCounter, TokenEstimate
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class BudgetPlan:
|
|
13
|
+
input_allowance: int
|
|
14
|
+
required_estimate: TokenEstimate
|
|
15
|
+
retrieval_tokens: int
|
|
16
|
+
summary_tokens: int
|
|
17
|
+
window_tokens: int
|
|
18
|
+
window_turn_ids: tuple[str, ...] = ()
|
|
19
|
+
|
|
20
|
+
def __post_init__(self) -> None:
|
|
21
|
+
integer_value("input_allowance", self.input_allowance, minimum=1)
|
|
22
|
+
if not isinstance(self.required_estimate, TokenEstimate):
|
|
23
|
+
raise ContractError("Budget plan requires a token estimate")
|
|
24
|
+
for name in ("retrieval_tokens", "summary_tokens", "window_tokens"):
|
|
25
|
+
integer_value(name, getattr(self, name))
|
|
26
|
+
ids = freeze_sequence(self, "window_turn_ids", str)
|
|
27
|
+
if len(set(ids)) != len(ids) or any(not identifier.strip() for identifier in ids):
|
|
28
|
+
raise ContractError("Window turn IDs must be unique and nonempty")
|
|
29
|
+
allocated = (
|
|
30
|
+
self.required_estimate.estimated_tokens
|
|
31
|
+
+ self.retrieval_tokens
|
|
32
|
+
+ self.summary_tokens
|
|
33
|
+
+ self.window_tokens
|
|
34
|
+
)
|
|
35
|
+
if allocated != self.input_allowance:
|
|
36
|
+
raise ContractError("Budget allocations must equal the input allowance")
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict:
|
|
39
|
+
return {
|
|
40
|
+
"input_allowance": self.input_allowance,
|
|
41
|
+
"required": self.required_estimate.to_dict(),
|
|
42
|
+
"retrieval_tokens": self.retrieval_tokens,
|
|
43
|
+
"summary_tokens": self.summary_tokens,
|
|
44
|
+
"window_tokens": self.window_tokens,
|
|
45
|
+
"window_turn_ids": list(self.window_turn_ids),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def plan_budget(
|
|
50
|
+
*, required_request: ChatRequest, config: BudgetConfig, counter: TokenCounter
|
|
51
|
+
) -> BudgetPlan:
|
|
52
|
+
"""Reserve a caller-supplied mandatory request without dropping any content.
|
|
53
|
+
|
|
54
|
+
The assembly layer must supply system, active pins, question and tool schemas.
|
|
55
|
+
Layer allocations use calibrated token units and include incremental framing;
|
|
56
|
+
they are not raw text-token limits. Assembly recounts the final whole request.
|
|
57
|
+
"""
|
|
58
|
+
estimate = counter.count_request(required_request)
|
|
59
|
+
allowance = config.input_allowance
|
|
60
|
+
remaining = allowance - estimate.estimated_tokens
|
|
61
|
+
if remaining < 0:
|
|
62
|
+
raise RequiredContextTooLarge(
|
|
63
|
+
"Required context exceeds the input allowance; "
|
|
64
|
+
"reduce supplied content or raise the budget",
|
|
65
|
+
required_tokens=estimate.estimated_tokens,
|
|
66
|
+
input_allowance=allowance,
|
|
67
|
+
overflow_tokens=-remaining,
|
|
68
|
+
)
|
|
69
|
+
reserve = config.retrieval_reserve + config.summary_reserve
|
|
70
|
+
if reserve > remaining:
|
|
71
|
+
raise LayerAllocationError(
|
|
72
|
+
"Layer reserves exceed space remaining after required context",
|
|
73
|
+
available_tokens=remaining,
|
|
74
|
+
reserved_tokens=reserve,
|
|
75
|
+
)
|
|
76
|
+
return BudgetPlan(
|
|
77
|
+
allowance, estimate, config.retrieval_reserve, config.summary_reserve, remaining - reserve
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validate_request(
|
|
82
|
+
*, request: ChatRequest, config: BudgetConfig, counter: TokenCounter
|
|
83
|
+
) -> TokenEstimate:
|
|
84
|
+
"""Recount an entire final request. Never infer safety from per-block sums."""
|
|
85
|
+
estimate = counter.count_request(request)
|
|
86
|
+
if estimate.estimated_tokens > config.input_allowance:
|
|
87
|
+
raise PromptTooLarge(
|
|
88
|
+
"Serialized request exceeds the estimated input allowance",
|
|
89
|
+
estimated_tokens=estimate.estimated_tokens,
|
|
90
|
+
input_allowance=config.input_allowance,
|
|
91
|
+
overflow_tokens=estimate.estimated_tokens - config.input_allowance,
|
|
92
|
+
)
|
|
93
|
+
return estimate
|