weakincentives 0.9.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.
- weakincentives/__init__.py +67 -0
- weakincentives/adapters/__init__.py +37 -0
- weakincentives/adapters/_names.py +32 -0
- weakincentives/adapters/_provider_protocols.py +69 -0
- weakincentives/adapters/_tool_messages.py +80 -0
- weakincentives/adapters/core.py +102 -0
- weakincentives/adapters/litellm.py +254 -0
- weakincentives/adapters/openai.py +254 -0
- weakincentives/adapters/shared.py +1021 -0
- weakincentives/cli/__init__.py +23 -0
- weakincentives/cli/wink.py +58 -0
- weakincentives/dbc/__init__.py +412 -0
- weakincentives/deadlines.py +58 -0
- weakincentives/prompt/__init__.py +105 -0
- weakincentives/prompt/_generic_params_specializer.py +64 -0
- weakincentives/prompt/_normalization.py +48 -0
- weakincentives/prompt/_overrides_protocols.py +33 -0
- weakincentives/prompt/_types.py +34 -0
- weakincentives/prompt/chapter.py +146 -0
- weakincentives/prompt/composition.py +281 -0
- weakincentives/prompt/errors.py +57 -0
- weakincentives/prompt/markdown.py +108 -0
- weakincentives/prompt/overrides/__init__.py +59 -0
- weakincentives/prompt/overrides/_fs.py +164 -0
- weakincentives/prompt/overrides/inspection.py +141 -0
- weakincentives/prompt/overrides/local_store.py +275 -0
- weakincentives/prompt/overrides/validation.py +534 -0
- weakincentives/prompt/overrides/versioning.py +269 -0
- weakincentives/prompt/prompt.py +353 -0
- weakincentives/prompt/protocols.py +103 -0
- weakincentives/prompt/registry.py +375 -0
- weakincentives/prompt/rendering.py +288 -0
- weakincentives/prompt/response_format.py +60 -0
- weakincentives/prompt/section.py +166 -0
- weakincentives/prompt/structured_output.py +179 -0
- weakincentives/prompt/tool.py +397 -0
- weakincentives/prompt/tool_result.py +30 -0
- weakincentives/py.typed +0 -0
- weakincentives/runtime/__init__.py +82 -0
- weakincentives/runtime/events/__init__.py +126 -0
- weakincentives/runtime/events/_types.py +110 -0
- weakincentives/runtime/logging.py +284 -0
- weakincentives/runtime/session/__init__.py +46 -0
- weakincentives/runtime/session/_slice_types.py +24 -0
- weakincentives/runtime/session/_types.py +55 -0
- weakincentives/runtime/session/dataclasses.py +29 -0
- weakincentives/runtime/session/protocols.py +34 -0
- weakincentives/runtime/session/reducer_context.py +40 -0
- weakincentives/runtime/session/reducers.py +82 -0
- weakincentives/runtime/session/selectors.py +56 -0
- weakincentives/runtime/session/session.py +387 -0
- weakincentives/runtime/session/snapshots.py +310 -0
- weakincentives/serde/__init__.py +19 -0
- weakincentives/serde/_utils.py +240 -0
- weakincentives/serde/dataclass_serde.py +55 -0
- weakincentives/serde/dump.py +189 -0
- weakincentives/serde/parse.py +417 -0
- weakincentives/serde/schema.py +260 -0
- weakincentives/tools/__init__.py +154 -0
- weakincentives/tools/_context.py +38 -0
- weakincentives/tools/asteval.py +853 -0
- weakincentives/tools/errors.py +26 -0
- weakincentives/tools/planning.py +831 -0
- weakincentives/tools/podman.py +1655 -0
- weakincentives/tools/subagents.py +346 -0
- weakincentives/tools/vfs.py +1390 -0
- weakincentives/types/__init__.py +35 -0
- weakincentives/types/json.py +45 -0
- weakincentives-0.9.0.dist-info/METADATA +775 -0
- weakincentives-0.9.0.dist-info/RECORD +73 -0
- weakincentives-0.9.0.dist-info/WHEEL +4 -0
- weakincentives-0.9.0.dist-info/entry_points.txt +2 -0
- weakincentives-0.9.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
2
|
+
# you may not use this file except in compliance with the License.
|
|
3
|
+
# You may obtain a copy of the License at
|
|
4
|
+
#
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
#
|
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
10
|
+
# See the License for the specific language governing permissions and
|
|
11
|
+
# limitations under the License.
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from hashlib import sha256
|
|
19
|
+
from typing import Literal, Protocol, TypeVar, overload
|
|
20
|
+
|
|
21
|
+
from ...serde.schema import schema
|
|
22
|
+
from .._types import SupportsDataclass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _section_override_mapping_factory() -> dict[tuple[str, ...], SectionOverride]:
|
|
26
|
+
return {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _tool_override_mapping_factory() -> dict[str, ToolOverride]:
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _param_description_mapping_factory() -> dict[str, str]:
|
|
34
|
+
return {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ToolContractProtocol(Protocol):
|
|
38
|
+
name: str
|
|
39
|
+
description: str
|
|
40
|
+
params_type: type[SupportsDataclass]
|
|
41
|
+
result_type: type[SupportsDataclass]
|
|
42
|
+
result_container: Literal["object", "array"]
|
|
43
|
+
accepts_overrides: bool
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SectionLike(Protocol):
|
|
47
|
+
def original_body_template(self) -> str | None: ...
|
|
48
|
+
|
|
49
|
+
def tools(self) -> tuple[ToolContractProtocol, ...]: ...
|
|
50
|
+
|
|
51
|
+
accepts_overrides: bool
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SectionNodeLike(Protocol):
|
|
55
|
+
path: tuple[str, ...]
|
|
56
|
+
section: SectionLike
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PromptLike(Protocol):
|
|
60
|
+
ns: str
|
|
61
|
+
key: str
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def sections(self) -> tuple[SectionNodeLike, ...]: ...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_HEX_DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
68
|
+
|
|
69
|
+
_HexDigestT = TypeVar("_HexDigestT", bound="HexDigest")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class HexDigest(str):
|
|
73
|
+
"""A validated lowercase hexadecimal SHA-256 digest."""
|
|
74
|
+
|
|
75
|
+
__slots__ = ()
|
|
76
|
+
|
|
77
|
+
def __new__(cls: type[_HexDigestT], value: object) -> _HexDigestT:
|
|
78
|
+
if not isinstance(value, str):
|
|
79
|
+
msg = "HexDigest value must be a string."
|
|
80
|
+
raise TypeError(msg)
|
|
81
|
+
if not _HEX_DIGEST_RE.fullmatch(value):
|
|
82
|
+
msg = f"Invalid hex digest value: {value!r}"
|
|
83
|
+
raise ValueError(msg)
|
|
84
|
+
return str.__new__(cls, value)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@overload
|
|
88
|
+
def ensure_hex_digest(value: HexDigest, *, field_name: str) -> HexDigest: ...
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@overload
|
|
92
|
+
def ensure_hex_digest(value: str, *, field_name: str) -> HexDigest: ...
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def ensure_hex_digest(value: object, *, field_name: str) -> HexDigest:
|
|
96
|
+
"""Normalize an object to a :class:`HexDigest` with helpful errors."""
|
|
97
|
+
|
|
98
|
+
if isinstance(value, HexDigest):
|
|
99
|
+
return value
|
|
100
|
+
if isinstance(value, str):
|
|
101
|
+
try:
|
|
102
|
+
return HexDigest(value)
|
|
103
|
+
except ValueError as error:
|
|
104
|
+
msg = f"{field_name} must be a 64 character lowercase hex digest."
|
|
105
|
+
raise PromptOverridesError(msg) from error
|
|
106
|
+
msg = f"{field_name} must be a string."
|
|
107
|
+
raise PromptOverridesError(msg)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(slots=True)
|
|
111
|
+
class SectionDescriptor:
|
|
112
|
+
"""Hash metadata for a single section within a prompt."""
|
|
113
|
+
|
|
114
|
+
path: tuple[str, ...]
|
|
115
|
+
content_hash: HexDigest
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(slots=True)
|
|
119
|
+
class ToolDescriptor:
|
|
120
|
+
"""Stable metadata describing a tool exposed by a prompt."""
|
|
121
|
+
|
|
122
|
+
path: tuple[str, ...]
|
|
123
|
+
name: str
|
|
124
|
+
contract_hash: HexDigest
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(slots=True)
|
|
128
|
+
class PromptDescriptor:
|
|
129
|
+
"""Stable metadata describing a prompt and its hash-aware sections."""
|
|
130
|
+
|
|
131
|
+
ns: str
|
|
132
|
+
key: str
|
|
133
|
+
sections: list[SectionDescriptor]
|
|
134
|
+
tools: list[ToolDescriptor]
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def from_prompt(cls, prompt: PromptLike) -> PromptDescriptor:
|
|
138
|
+
sections: list[SectionDescriptor] = []
|
|
139
|
+
tools: list[ToolDescriptor] = []
|
|
140
|
+
for node in prompt.sections:
|
|
141
|
+
if getattr(node.section, "accepts_overrides", True):
|
|
142
|
+
template = node.section.original_body_template()
|
|
143
|
+
if template is not None:
|
|
144
|
+
content_hash = hash_text(template)
|
|
145
|
+
sections.append(SectionDescriptor(node.path, content_hash))
|
|
146
|
+
tool_descriptors = [
|
|
147
|
+
ToolDescriptor(
|
|
148
|
+
path=node.path,
|
|
149
|
+
name=tool.name,
|
|
150
|
+
contract_hash=_tool_contract_hash(tool),
|
|
151
|
+
)
|
|
152
|
+
for tool in node.section.tools()
|
|
153
|
+
if tool.accepts_overrides
|
|
154
|
+
]
|
|
155
|
+
tools.extend(tool_descriptors)
|
|
156
|
+
return cls(prompt.ns, prompt.key, sections, tools)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass(slots=True)
|
|
160
|
+
class SectionOverride:
|
|
161
|
+
"""Override payload for a prompt section validated by hash."""
|
|
162
|
+
|
|
163
|
+
expected_hash: HexDigest
|
|
164
|
+
body: str
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(slots=True)
|
|
168
|
+
class ToolOverride:
|
|
169
|
+
"""Description overrides validated against a tool contract hash."""
|
|
170
|
+
|
|
171
|
+
name: str
|
|
172
|
+
expected_contract_hash: HexDigest
|
|
173
|
+
description: str | None = None
|
|
174
|
+
param_descriptions: dict[str, str] = field(
|
|
175
|
+
default_factory=_param_description_mapping_factory
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass(slots=True)
|
|
180
|
+
class PromptOverride:
|
|
181
|
+
"""Runtime replacements for prompt sections validated by an overrides store."""
|
|
182
|
+
|
|
183
|
+
ns: str
|
|
184
|
+
prompt_key: str
|
|
185
|
+
tag: str
|
|
186
|
+
sections: dict[tuple[str, ...], SectionOverride] = field(
|
|
187
|
+
default_factory=_section_override_mapping_factory
|
|
188
|
+
)
|
|
189
|
+
tool_overrides: dict[str, ToolOverride] = field(
|
|
190
|
+
default_factory=_tool_override_mapping_factory
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class PromptOverridesError(Exception):
|
|
195
|
+
"""Raised when prompt overrides fail validation or persistence."""
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class PromptOverridesStore(Protocol):
|
|
199
|
+
"""Lookup interface for resolving prompt overrides at render time."""
|
|
200
|
+
|
|
201
|
+
def resolve(
|
|
202
|
+
self,
|
|
203
|
+
descriptor: PromptDescriptor,
|
|
204
|
+
tag: str = "latest",
|
|
205
|
+
) -> PromptOverride | None: ...
|
|
206
|
+
|
|
207
|
+
def upsert(
|
|
208
|
+
self,
|
|
209
|
+
descriptor: PromptDescriptor,
|
|
210
|
+
override: PromptOverride,
|
|
211
|
+
) -> PromptOverride: ...
|
|
212
|
+
|
|
213
|
+
def delete(
|
|
214
|
+
self,
|
|
215
|
+
*,
|
|
216
|
+
ns: str,
|
|
217
|
+
prompt_key: str,
|
|
218
|
+
tag: str,
|
|
219
|
+
) -> None: ...
|
|
220
|
+
|
|
221
|
+
def seed_if_necessary(
|
|
222
|
+
self,
|
|
223
|
+
prompt: PromptLike,
|
|
224
|
+
*,
|
|
225
|
+
tag: str = "latest",
|
|
226
|
+
) -> PromptOverride: ...
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
__all__ = [
|
|
230
|
+
"HexDigest",
|
|
231
|
+
"PromptDescriptor",
|
|
232
|
+
"PromptOverride",
|
|
233
|
+
"PromptOverridesError",
|
|
234
|
+
"PromptOverridesStore",
|
|
235
|
+
"SectionDescriptor",
|
|
236
|
+
"SectionOverride",
|
|
237
|
+
"ToolDescriptor",
|
|
238
|
+
"ToolOverride",
|
|
239
|
+
"ensure_hex_digest",
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _tool_contract_hash(tool: ToolContractProtocol) -> HexDigest:
|
|
244
|
+
description_hash = hash_text(tool.description)
|
|
245
|
+
params_schema_hash = hash_json(schema(tool.params_type, extra="forbid"))
|
|
246
|
+
if getattr(tool, "result_container", "object") == "array":
|
|
247
|
+
item_schema = schema(tool.result_type, extra="ignore")
|
|
248
|
+
result_schema = {
|
|
249
|
+
"title": f"{tool.result_type.__name__}List",
|
|
250
|
+
"type": "array",
|
|
251
|
+
"items": item_schema,
|
|
252
|
+
}
|
|
253
|
+
else:
|
|
254
|
+
result_schema = schema(tool.result_type, extra="ignore")
|
|
255
|
+
result_schema_hash = hash_json(result_schema)
|
|
256
|
+
return hash_text(
|
|
257
|
+
"::".join((description_hash, params_schema_hash, result_schema_hash))
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def hash_text(value: str) -> HexDigest:
|
|
262
|
+
return HexDigest(sha256(value.encode("utf-8")).hexdigest())
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def hash_json(value: object) -> HexDigest:
|
|
266
|
+
canonical = json.dumps(
|
|
267
|
+
value, sort_keys=True, separators=(",", ":"), ensure_ascii=True
|
|
268
|
+
)
|
|
269
|
+
return hash_text(canonical)
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
2
|
+
# you may not use this file except in compliance with the License.
|
|
3
|
+
# You may obtain a copy of the License at
|
|
4
|
+
#
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
#
|
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
10
|
+
# See the License for the specific language governing permissions and
|
|
11
|
+
# limitations under the License.
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Mapping, Sequence
|
|
16
|
+
from dataclasses import is_dataclass
|
|
17
|
+
from typing import (
|
|
18
|
+
TYPE_CHECKING,
|
|
19
|
+
Any,
|
|
20
|
+
ClassVar,
|
|
21
|
+
Literal,
|
|
22
|
+
cast,
|
|
23
|
+
get_args,
|
|
24
|
+
get_origin,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from ._overrides_protocols import PromptOverridesStoreProtocol
|
|
28
|
+
from ._types import SupportsDataclass
|
|
29
|
+
from .chapter import Chapter, ChaptersExpansionPolicy
|
|
30
|
+
from .errors import PromptValidationError, SectionPath
|
|
31
|
+
from .registry import PromptRegistry, RegistrySnapshot, SectionNode, clone_dataclass
|
|
32
|
+
from .rendering import PromptRenderer, RenderedPrompt
|
|
33
|
+
from .response_format import ResponseFormatParams, ResponseFormatSection
|
|
34
|
+
from .section import Section
|
|
35
|
+
from .structured_output import StructuredOutputConfig
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from .overrides import PromptLike, ToolOverride
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _format_specialization_argument(argument: object | None) -> str:
|
|
42
|
+
if argument is None:
|
|
43
|
+
return "?"
|
|
44
|
+
if isinstance(argument, type):
|
|
45
|
+
return argument.__name__
|
|
46
|
+
return repr(argument)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Prompt[OutputT]:
|
|
50
|
+
"""Coordinate prompt sections and their parameter bindings."""
|
|
51
|
+
|
|
52
|
+
_output_container_spec: ClassVar[Literal["object", "array"] | None] = None
|
|
53
|
+
_output_dataclass_candidate: ClassVar[Any] = None
|
|
54
|
+
|
|
55
|
+
def __class_getitem__(cls, item: object) -> type[Prompt[Any]]:
|
|
56
|
+
origin = get_origin(item)
|
|
57
|
+
candidate = item
|
|
58
|
+
container: Literal["object", "array"] | None = "object"
|
|
59
|
+
|
|
60
|
+
if origin is list:
|
|
61
|
+
args = get_args(item)
|
|
62
|
+
candidate = args[0] if len(args) == 1 else None
|
|
63
|
+
container = "array"
|
|
64
|
+
label = f"list[{_format_specialization_argument(candidate)}]"
|
|
65
|
+
else:
|
|
66
|
+
container = "object"
|
|
67
|
+
label = _format_specialization_argument(candidate)
|
|
68
|
+
|
|
69
|
+
name = f"{cls.__name__}[{label}]"
|
|
70
|
+
namespace = {
|
|
71
|
+
"__module__": cls.__module__,
|
|
72
|
+
"_output_container_spec": container if candidate is not None else None,
|
|
73
|
+
"_output_dataclass_candidate": candidate,
|
|
74
|
+
}
|
|
75
|
+
return type(name, (cls,), namespace)
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
*,
|
|
80
|
+
ns: str,
|
|
81
|
+
key: str,
|
|
82
|
+
name: str | None = None,
|
|
83
|
+
sections: Sequence[Section[SupportsDataclass]] | None = None,
|
|
84
|
+
chapters: Sequence[Chapter[SupportsDataclass]] | None = None,
|
|
85
|
+
inject_output_instructions: bool = True,
|
|
86
|
+
allow_extra_keys: bool = False,
|
|
87
|
+
) -> None:
|
|
88
|
+
super().__init__()
|
|
89
|
+
stripped_ns = ns.strip()
|
|
90
|
+
if not stripped_ns:
|
|
91
|
+
raise PromptValidationError("Prompt namespace must be a non-empty string.")
|
|
92
|
+
stripped_key = key.strip()
|
|
93
|
+
if not stripped_key:
|
|
94
|
+
raise PromptValidationError("Prompt key must be a non-empty string.")
|
|
95
|
+
self.ns = stripped_ns
|
|
96
|
+
self.key = stripped_key
|
|
97
|
+
self.name = name
|
|
98
|
+
base_sections = tuple(sections or ())
|
|
99
|
+
self._base_sections: tuple[Section[SupportsDataclass], ...] = base_sections
|
|
100
|
+
self._sections: tuple[Section[SupportsDataclass], ...] = base_sections
|
|
101
|
+
self._registry = PromptRegistry()
|
|
102
|
+
self.placeholders: dict[SectionPath, set[str]] = {}
|
|
103
|
+
self._allow_extra_keys_requested = allow_extra_keys
|
|
104
|
+
|
|
105
|
+
seen_chapter_keys: set[str] = set()
|
|
106
|
+
provided_chapters = tuple(chapters or ())
|
|
107
|
+
for chapter in provided_chapters:
|
|
108
|
+
if chapter.key in seen_chapter_keys:
|
|
109
|
+
raise PromptValidationError(
|
|
110
|
+
"Prompt chapters must use unique keys.",
|
|
111
|
+
section_path=(chapter.key,),
|
|
112
|
+
)
|
|
113
|
+
seen_chapter_keys.add(chapter.key)
|
|
114
|
+
self._chapters: tuple[Chapter[SupportsDataclass], ...] = provided_chapters
|
|
115
|
+
self._chapter_key_registry: dict[str, Chapter[SupportsDataclass]] = {
|
|
116
|
+
chapter.key: chapter for chapter in self._chapters
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
self._structured_output: StructuredOutputConfig[SupportsDataclass] | None
|
|
120
|
+
self._structured_output = self._resolve_output_spec(allow_extra_keys)
|
|
121
|
+
|
|
122
|
+
self.inject_output_instructions = inject_output_instructions
|
|
123
|
+
|
|
124
|
+
self._registry.register_sections(self._sections)
|
|
125
|
+
|
|
126
|
+
self._response_section: ResponseFormatSection | None = None
|
|
127
|
+
if self._structured_output is not None:
|
|
128
|
+
response_params = self._build_response_format_params()
|
|
129
|
+
response_section = ResponseFormatSection(
|
|
130
|
+
params=response_params,
|
|
131
|
+
enabled=lambda _params, prompt=self: prompt.inject_output_instructions,
|
|
132
|
+
)
|
|
133
|
+
self._response_section = response_section
|
|
134
|
+
section_for_registry = cast(Section[SupportsDataclass], response_section)
|
|
135
|
+
self._sections += (section_for_registry,)
|
|
136
|
+
self._registry.register_section(
|
|
137
|
+
section_for_registry, path=(response_section.key,), depth=0
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
snapshot = self._registry.snapshot()
|
|
141
|
+
self._registry_snapshot: RegistrySnapshot = snapshot
|
|
142
|
+
self.placeholders = {
|
|
143
|
+
path: set(names) for path, names in snapshot.placeholders.items()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
self._renderer: PromptRenderer[OutputT] = PromptRenderer(
|
|
147
|
+
registry=snapshot,
|
|
148
|
+
structured_output=self._structured_output,
|
|
149
|
+
response_section=self._response_section,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def render(
|
|
153
|
+
self,
|
|
154
|
+
*params: SupportsDataclass,
|
|
155
|
+
overrides_store: PromptOverridesStoreProtocol | None = None,
|
|
156
|
+
tag: str = "latest",
|
|
157
|
+
inject_output_instructions: bool | None = None,
|
|
158
|
+
) -> RenderedPrompt[OutputT]:
|
|
159
|
+
"""Render the prompt and apply overrides when an overrides store is supplied."""
|
|
160
|
+
|
|
161
|
+
overrides: dict[SectionPath, str] | None = None
|
|
162
|
+
tool_overrides: dict[str, ToolOverride] | None = None
|
|
163
|
+
|
|
164
|
+
if overrides_store is not None:
|
|
165
|
+
from .overrides import PromptDescriptor
|
|
166
|
+
|
|
167
|
+
descriptor = PromptDescriptor.from_prompt(cast("PromptLike", self))
|
|
168
|
+
override = overrides_store.resolve(descriptor=descriptor, tag=tag)
|
|
169
|
+
|
|
170
|
+
if override is not None:
|
|
171
|
+
overrides = {
|
|
172
|
+
path: section_override.body
|
|
173
|
+
for path, section_override in override.sections.items()
|
|
174
|
+
}
|
|
175
|
+
tool_overrides = dict(override.tool_overrides)
|
|
176
|
+
|
|
177
|
+
param_lookup = self._renderer.build_param_lookup(params)
|
|
178
|
+
return self._renderer.render(
|
|
179
|
+
param_lookup,
|
|
180
|
+
overrides,
|
|
181
|
+
tool_overrides,
|
|
182
|
+
inject_output_instructions=inject_output_instructions,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def sections(self) -> tuple[SectionNode[SupportsDataclass], ...]:
|
|
187
|
+
return self._registry_snapshot.sections
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def param_types(self) -> set[type[SupportsDataclass]]:
|
|
191
|
+
return self._registry_snapshot.param_types
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def structured_output(self) -> StructuredOutputConfig[SupportsDataclass] | None:
|
|
195
|
+
"""Resolved structured output declaration, when present."""
|
|
196
|
+
|
|
197
|
+
return self._structured_output
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def chapters(self) -> tuple[Chapter[SupportsDataclass], ...]:
|
|
201
|
+
return self._chapters
|
|
202
|
+
|
|
203
|
+
def expand_chapters(
|
|
204
|
+
self,
|
|
205
|
+
policy: ChaptersExpansionPolicy,
|
|
206
|
+
*,
|
|
207
|
+
chapter_params: Mapping[str, SupportsDataclass | None] | None = None,
|
|
208
|
+
) -> Prompt[OutputT]:
|
|
209
|
+
"""Return a prompt snapshot with chapters opened per the supplied policy."""
|
|
210
|
+
|
|
211
|
+
if not self._chapters:
|
|
212
|
+
return self
|
|
213
|
+
|
|
214
|
+
if policy is ChaptersExpansionPolicy.ALL_INCLUDED:
|
|
215
|
+
return self._expand_chapters_all_included(
|
|
216
|
+
chapter_params=chapter_params or {}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
raise NotImplementedError(
|
|
220
|
+
f"Chapters expansion policy '{policy.value}' is not supported."
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def _expand_chapters_all_included(
|
|
224
|
+
self,
|
|
225
|
+
*,
|
|
226
|
+
chapter_params: Mapping[str, SupportsDataclass | None],
|
|
227
|
+
) -> Prompt[OutputT]:
|
|
228
|
+
provided_lookup = dict(chapter_params)
|
|
229
|
+
unknown_keys = set(provided_lookup) - set(self._chapter_key_registry.keys())
|
|
230
|
+
if unknown_keys:
|
|
231
|
+
unknown_key = sorted(unknown_keys)[0]
|
|
232
|
+
raise PromptValidationError(
|
|
233
|
+
"Chapter parameters reference unknown chapter key.",
|
|
234
|
+
section_path=(unknown_key,),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
open_sections: list[Section[SupportsDataclass]] = list(self._base_sections)
|
|
238
|
+
|
|
239
|
+
for chapter in self._chapters:
|
|
240
|
+
key_present = chapter.key in provided_lookup
|
|
241
|
+
params = provided_lookup.get(chapter.key)
|
|
242
|
+
if key_present:
|
|
243
|
+
params = self._normalize_chapter_params(chapter, params)
|
|
244
|
+
elif chapter.default_params is not None:
|
|
245
|
+
params = clone_dataclass(chapter.default_params)
|
|
246
|
+
|
|
247
|
+
if chapter.enabled is not None:
|
|
248
|
+
if params is None and chapter.param_type is not None:
|
|
249
|
+
raise PromptValidationError(
|
|
250
|
+
"Chapter requires parameters for enabled predicate.",
|
|
251
|
+
section_path=(chapter.key,),
|
|
252
|
+
dataclass_type=chapter.param_type,
|
|
253
|
+
)
|
|
254
|
+
try:
|
|
255
|
+
enabled = chapter.is_enabled(params)
|
|
256
|
+
except Exception as error:
|
|
257
|
+
raise PromptValidationError(
|
|
258
|
+
"Chapter enabled predicate failed.",
|
|
259
|
+
section_path=(chapter.key,),
|
|
260
|
+
dataclass_type=chapter.param_type,
|
|
261
|
+
) from error
|
|
262
|
+
if not enabled:
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
open_sections.extend(chapter.sections)
|
|
266
|
+
|
|
267
|
+
prompt_cls = type(self)
|
|
268
|
+
|
|
269
|
+
return prompt_cls(
|
|
270
|
+
ns=self.ns,
|
|
271
|
+
key=self.key,
|
|
272
|
+
name=self.name,
|
|
273
|
+
sections=open_sections,
|
|
274
|
+
chapters=(),
|
|
275
|
+
inject_output_instructions=self.inject_output_instructions,
|
|
276
|
+
allow_extra_keys=self._allow_extra_keys_requested,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def _normalize_chapter_params(
|
|
280
|
+
self,
|
|
281
|
+
chapter: Chapter[SupportsDataclass],
|
|
282
|
+
params: SupportsDataclass | None,
|
|
283
|
+
) -> SupportsDataclass | None:
|
|
284
|
+
params_type = chapter.param_type
|
|
285
|
+
if params_type is None:
|
|
286
|
+
if params is not None:
|
|
287
|
+
raise PromptValidationError(
|
|
288
|
+
"Chapter does not accept parameters.",
|
|
289
|
+
section_path=(chapter.key,),
|
|
290
|
+
)
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
if params is None:
|
|
294
|
+
raise PromptValidationError(
|
|
295
|
+
"Chapter requires parameters.",
|
|
296
|
+
section_path=(chapter.key,),
|
|
297
|
+
dataclass_type=params_type,
|
|
298
|
+
)
|
|
299
|
+
return params
|
|
300
|
+
|
|
301
|
+
def _resolve_output_spec(
|
|
302
|
+
self, allow_extra_keys: bool
|
|
303
|
+
) -> StructuredOutputConfig[SupportsDataclass] | None:
|
|
304
|
+
candidate = getattr(type(self), "_output_dataclass_candidate", None)
|
|
305
|
+
container = cast(
|
|
306
|
+
Literal["object", "array"] | None,
|
|
307
|
+
getattr(type(self), "_output_container_spec", None),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if candidate is None or container is None:
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
if not isinstance(candidate, type):
|
|
314
|
+
candidate_type = cast(type[Any], type(candidate))
|
|
315
|
+
raise PromptValidationError(
|
|
316
|
+
"Prompt output type must be a dataclass.",
|
|
317
|
+
dataclass_type=candidate_type,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
if not is_dataclass(candidate):
|
|
321
|
+
bad_dataclass = cast(type[Any], candidate)
|
|
322
|
+
raise PromptValidationError(
|
|
323
|
+
"Prompt output type must be a dataclass.",
|
|
324
|
+
dataclass_type=bad_dataclass,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
dataclass_type = cast(type[SupportsDataclass], candidate)
|
|
328
|
+
return StructuredOutputConfig(
|
|
329
|
+
dataclass_type=dataclass_type,
|
|
330
|
+
container=container,
|
|
331
|
+
allow_extra_keys=allow_extra_keys,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def _build_response_format_params(self) -> ResponseFormatParams:
|
|
335
|
+
spec = self._structured_output
|
|
336
|
+
if spec is None:
|
|
337
|
+
raise RuntimeError(
|
|
338
|
+
"Output container missing during response format construction."
|
|
339
|
+
)
|
|
340
|
+
container = spec.container
|
|
341
|
+
|
|
342
|
+
article: Literal["a", "an"] = (
|
|
343
|
+
"an" if container.startswith(("a", "e", "i", "o", "u")) else "a"
|
|
344
|
+
)
|
|
345
|
+
extra_clause = "." if spec.allow_extra_keys else ". Do not add extra keys."
|
|
346
|
+
return ResponseFormatParams(
|
|
347
|
+
article=article,
|
|
348
|
+
container=container,
|
|
349
|
+
extra_clause=extra_clause,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
__all__ = ["Prompt", "RenderedPrompt", "SectionNode"]
|