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,64 @@
|
|
|
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 typing import ClassVar, Generic, TypeVar, cast
|
|
16
|
+
|
|
17
|
+
from ._types import SupportsDataclass
|
|
18
|
+
|
|
19
|
+
ParamsT = TypeVar("ParamsT", bound=SupportsDataclass, covariant=True)
|
|
20
|
+
|
|
21
|
+
SelfClass = TypeVar(
|
|
22
|
+
"SelfClass",
|
|
23
|
+
bound="GenericParamsSpecializer[SupportsDataclass]",
|
|
24
|
+
covariant=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GenericParamsSpecializer(Generic[ParamsT]): # noqa: UP046
|
|
29
|
+
"""Mixin providing ``ParamsT`` specialization for prompt components."""
|
|
30
|
+
|
|
31
|
+
_params_type: ClassVar[type[SupportsDataclass] | None] = None
|
|
32
|
+
_generic_owner_name: ClassVar[str | None] = None
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def __class_getitem__(cls: type[SelfClass], item: object) -> type[SelfClass]:
|
|
36
|
+
params_type = cls._normalize_generic_argument(item)
|
|
37
|
+
specialized = cast(
|
|
38
|
+
"type[SelfClass]",
|
|
39
|
+
type(cls.__name__, (cls,), {}),
|
|
40
|
+
)
|
|
41
|
+
specialized.__name__ = cls.__name__
|
|
42
|
+
specialized.__qualname__ = cls.__qualname__
|
|
43
|
+
specialized.__module__ = cls.__module__
|
|
44
|
+
specialized._params_type = cast(type[SupportsDataclass], params_type)
|
|
45
|
+
return specialized
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def _normalize_generic_argument(cls, item: object) -> object:
|
|
49
|
+
if isinstance(item, tuple):
|
|
50
|
+
raise TypeError(f"{cls._owner_name()}[...] expects a single type argument.")
|
|
51
|
+
return item
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def _owner_name(cls) -> str:
|
|
55
|
+
owner_name = getattr(cls, "_generic_owner_name", None)
|
|
56
|
+
if isinstance(owner_name, str) and owner_name:
|
|
57
|
+
return owner_name
|
|
58
|
+
name = getattr(cls, "__name__", None)
|
|
59
|
+
if isinstance(name, str) and name:
|
|
60
|
+
return name
|
|
61
|
+
return "Component"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__ = ["GenericParamsSpecializer"]
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
"""Shared utilities for normalizing prompt component identifiers."""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from typing import Final
|
|
19
|
+
|
|
20
|
+
COMPONENT_KEY_PATTERN: Final[re.Pattern[str]] = re.compile(
|
|
21
|
+
r"^[a-z0-9][a-z0-9._-]{0,63}$"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalize_component_key(key: str, *, owner: str) -> str:
|
|
26
|
+
"""Normalize component keys across prompt primitives.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
key: The user supplied identifier that should be normalized.
|
|
30
|
+
owner: Human-friendly label for the component requesting normalization.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The sanitized key that downstream consumers may safely use.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: When ``key`` is empty or fails to match
|
|
37
|
+
:data:`COMPONENT_KEY_PATTERN`.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
normalized = key.strip().lower()
|
|
41
|
+
if not normalized:
|
|
42
|
+
raise ValueError(f"{owner} key must be a non-empty string.")
|
|
43
|
+
if not COMPONENT_KEY_PATTERN.match(normalized):
|
|
44
|
+
raise ValueError(f"{owner} key must match {COMPONENT_KEY_PATTERN.pattern}.")
|
|
45
|
+
return normalized
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = ["COMPONENT_KEY_PATTERN", "normalize_component_key"]
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
"""Lightweight interfaces shared between prompt overrides and protocols."""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING, Protocol
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
20
|
+
from .overrides import PromptDescriptor, PromptOverride
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PromptOverridesStoreProtocol(Protocol):
|
|
24
|
+
"""Structural interface satisfied by prompt overrides stores."""
|
|
25
|
+
|
|
26
|
+
def resolve(
|
|
27
|
+
self,
|
|
28
|
+
descriptor: PromptDescriptor,
|
|
29
|
+
tag: str = "latest",
|
|
30
|
+
) -> PromptOverride | None: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ["PromptOverridesStoreProtocol"]
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
"""Internal typing helpers for the :mod:`weakincentives.prompt` package."""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from dataclasses import Field
|
|
19
|
+
from typing import Any, ClassVar, Protocol, runtime_checkable
|
|
20
|
+
|
|
21
|
+
type DataclassFieldMapping = dict[str, Field[Any]]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@runtime_checkable
|
|
25
|
+
class SupportsDataclass(Protocol):
|
|
26
|
+
"""Protocol satisfied by dataclass types and instances."""
|
|
27
|
+
|
|
28
|
+
__dataclass_fields__: ClassVar[DataclassFieldMapping]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
SupportsToolResult = SupportsDataclass | Sequence[SupportsDataclass]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ["DataclassFieldMapping", "SupportsDataclass", "SupportsToolResult"]
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
"""Chapter primitives controlling coarse-grained prompt visibility."""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import inspect
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from enum import StrEnum
|
|
21
|
+
from typing import ClassVar, TypeVar, cast
|
|
22
|
+
|
|
23
|
+
from ._generic_params_specializer import GenericParamsSpecializer
|
|
24
|
+
from ._normalization import normalize_component_key
|
|
25
|
+
from ._types import SupportsDataclass
|
|
26
|
+
from .section import Section
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ChaptersExpansionPolicy(StrEnum):
|
|
30
|
+
"""Strategies describing how adapters may open prompt chapters."""
|
|
31
|
+
|
|
32
|
+
ALL_INCLUDED = "all_included"
|
|
33
|
+
INTENT_CLASSIFIER = "intent_classifier"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
ChapterParamsT = TypeVar("ChapterParamsT", bound=SupportsDataclass, covariant=True)
|
|
37
|
+
EnabledPredicate = Callable[[SupportsDataclass], bool] | Callable[[], bool]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Chapter(GenericParamsSpecializer[ChapterParamsT]):
|
|
42
|
+
"""Container grouping sections under a shared visibility boundary."""
|
|
43
|
+
|
|
44
|
+
key: str
|
|
45
|
+
title: str
|
|
46
|
+
description: str | None = None
|
|
47
|
+
sections: tuple[Section[SupportsDataclass], ...] = ()
|
|
48
|
+
default_params: ChapterParamsT | None = None
|
|
49
|
+
enabled: EnabledPredicate | None = None
|
|
50
|
+
_enabled_callable: Callable[[SupportsDataclass | None], bool] | None = field(
|
|
51
|
+
init=False, repr=False, default=None
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
_generic_owner_name: ClassVar[str | None] = "Chapter"
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
params_candidate = getattr(self.__class__, "_params_type", None)
|
|
58
|
+
candidate_type = (
|
|
59
|
+
params_candidate if isinstance(params_candidate, type) else None
|
|
60
|
+
)
|
|
61
|
+
params_type = cast(type[SupportsDataclass] | None, candidate_type)
|
|
62
|
+
self.key = self._normalize_key(self.key)
|
|
63
|
+
|
|
64
|
+
self.sections = tuple(self.sections or ())
|
|
65
|
+
|
|
66
|
+
self._enabled_callable = self._normalize_enabled(self.enabled, params_type)
|
|
67
|
+
|
|
68
|
+
if params_type is None:
|
|
69
|
+
if self.default_params is not None:
|
|
70
|
+
raise TypeError(
|
|
71
|
+
"Chapter without parameters cannot define default_params."
|
|
72
|
+
)
|
|
73
|
+
elif self.default_params is not None and not isinstance(
|
|
74
|
+
self.default_params, params_type
|
|
75
|
+
):
|
|
76
|
+
raise TypeError(
|
|
77
|
+
"Chapter default_params must match the declared ParamsT type."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def params_type(self) -> type[ChapterParamsT] | None:
|
|
82
|
+
params_candidate = getattr(self.__class__, "_params_type", None)
|
|
83
|
+
candidate_type = (
|
|
84
|
+
params_candidate if isinstance(params_candidate, type) else None
|
|
85
|
+
)
|
|
86
|
+
return cast(type[ChapterParamsT] | None, candidate_type)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def param_type(self) -> type[ChapterParamsT] | None:
|
|
90
|
+
return self.params_type
|
|
91
|
+
|
|
92
|
+
def is_enabled(self, params: SupportsDataclass | None) -> bool:
|
|
93
|
+
"""Return True when the chapter should open for the provided params."""
|
|
94
|
+
|
|
95
|
+
if self._enabled_callable is None:
|
|
96
|
+
return True
|
|
97
|
+
if params is None and self.param_type is not None:
|
|
98
|
+
raise TypeError("Chapter parameters are required for enabled predicates.")
|
|
99
|
+
return bool(self._enabled_callable(params))
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _normalize_key(key: str) -> str:
|
|
103
|
+
return normalize_component_key(key, owner="Chapter")
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def _normalize_enabled(
|
|
107
|
+
enabled: EnabledPredicate | None,
|
|
108
|
+
params_type: type[SupportsDataclass] | None,
|
|
109
|
+
) -> Callable[[SupportsDataclass | None], bool] | None:
|
|
110
|
+
if enabled is None:
|
|
111
|
+
return None
|
|
112
|
+
if params_type is None and not _callable_requires_positional_argument(enabled):
|
|
113
|
+
zero_arg = cast(Callable[[], bool], enabled)
|
|
114
|
+
|
|
115
|
+
def _without_params(_: SupportsDataclass | None) -> bool:
|
|
116
|
+
return bool(zero_arg())
|
|
117
|
+
|
|
118
|
+
return _without_params
|
|
119
|
+
|
|
120
|
+
coerced = cast(Callable[[SupportsDataclass], bool], enabled)
|
|
121
|
+
|
|
122
|
+
def _with_params(value: SupportsDataclass | None) -> bool:
|
|
123
|
+
return bool(coerced(cast(SupportsDataclass, value)))
|
|
124
|
+
|
|
125
|
+
return _with_params
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _callable_requires_positional_argument(callback: EnabledPredicate) -> bool:
|
|
129
|
+
try:
|
|
130
|
+
signature = inspect.signature(callback)
|
|
131
|
+
except (TypeError, ValueError):
|
|
132
|
+
return True
|
|
133
|
+
for parameter in signature.parameters.values():
|
|
134
|
+
if (
|
|
135
|
+
parameter.kind
|
|
136
|
+
in (
|
|
137
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
138
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
139
|
+
)
|
|
140
|
+
and parameter.default is inspect.Signature.empty
|
|
141
|
+
):
|
|
142
|
+
return True
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
__all__ = ["Chapter", "ChaptersExpansionPolicy"]
|
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
"""Utilities for composing delegation prompts from rendered parents."""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Mapping, Sequence
|
|
18
|
+
from dataclasses import dataclass, field, replace
|
|
19
|
+
from types import MappingProxyType
|
|
20
|
+
from typing import Any, ClassVar, Generic, TypeVar, cast, override
|
|
21
|
+
|
|
22
|
+
from ._types import SupportsDataclass
|
|
23
|
+
from .errors import PromptRenderError
|
|
24
|
+
from .markdown import MarkdownSection
|
|
25
|
+
from .prompt import Prompt, RenderedPrompt
|
|
26
|
+
from .protocols import PromptProtocol
|
|
27
|
+
from .response_format import ResponseFormatParams, ResponseFormatSection
|
|
28
|
+
from .section import Section
|
|
29
|
+
|
|
30
|
+
ParentOutputT = TypeVar("ParentOutputT")
|
|
31
|
+
DelegationOutputT = TypeVar("DelegationOutputT")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(slots=True)
|
|
35
|
+
class DelegationParams:
|
|
36
|
+
"""Delegation summary fields surfaced to the delegated agent."""
|
|
37
|
+
|
|
38
|
+
reason: str
|
|
39
|
+
expected_result: str
|
|
40
|
+
may_delegate_further: str
|
|
41
|
+
recap_lines: tuple[str, ...] = field(default_factory=tuple)
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
self.recap_lines = tuple(self.recap_lines)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(slots=True)
|
|
48
|
+
class ParentPromptParams:
|
|
49
|
+
"""Container for the verbatim parent prompt body."""
|
|
50
|
+
|
|
51
|
+
body: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(slots=True)
|
|
55
|
+
class RecapParams:
|
|
56
|
+
"""Bullet-style recap directives rendered after the parent prompt."""
|
|
57
|
+
|
|
58
|
+
bullets: tuple[str, ...]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DelegationSummarySection(MarkdownSection[DelegationParams]):
|
|
62
|
+
"""Delegation summary rendered as a fixed bullet list."""
|
|
63
|
+
|
|
64
|
+
def __init__(self) -> None:
|
|
65
|
+
super().__init__(
|
|
66
|
+
title="Delegation Summary",
|
|
67
|
+
key="delegation-summary",
|
|
68
|
+
template=(
|
|
69
|
+
"- **Reason** - ${reason}\n"
|
|
70
|
+
"- **Expected result** - ${expected_result}\n"
|
|
71
|
+
"- **May delegate further?** - ${may_delegate_further}"
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ParentPromptSection(Section[ParentPromptParams]):
|
|
77
|
+
"""Embed the parent prompt verbatim between explicit markers."""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
*,
|
|
82
|
+
tools: Sequence[object] | None = None,
|
|
83
|
+
default_params: ParentPromptParams | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
super().__init__(
|
|
86
|
+
title="Parent Prompt (Verbatim)",
|
|
87
|
+
key="parent-prompt",
|
|
88
|
+
tools=tools,
|
|
89
|
+
default_params=default_params,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@override
|
|
93
|
+
def render(self, params: SupportsDataclass | None, depth: int) -> str:
|
|
94
|
+
if not isinstance(params, ParentPromptParams):
|
|
95
|
+
raise PromptRenderError(
|
|
96
|
+
"Parent prompt section requires parameters.",
|
|
97
|
+
dataclass_type=ParentPromptParams,
|
|
98
|
+
)
|
|
99
|
+
heading = "#" * (depth + 2)
|
|
100
|
+
prefix = f"{heading} {self.title}"
|
|
101
|
+
body = params.body
|
|
102
|
+
suffix = "" if body.endswith("\n") else "\n"
|
|
103
|
+
return (
|
|
104
|
+
f"{prefix}\n\n"
|
|
105
|
+
"<!-- PARENT PROMPT START -->\n"
|
|
106
|
+
f"{body}{suffix}"
|
|
107
|
+
"<!-- PARENT PROMPT END -->"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class RecapSection(Section[RecapParams]):
|
|
112
|
+
"""Render a concise recap of inherited parent prompt directives."""
|
|
113
|
+
|
|
114
|
+
def __init__(self) -> None:
|
|
115
|
+
super().__init__(title="Recap", key="recap")
|
|
116
|
+
|
|
117
|
+
@override
|
|
118
|
+
def render(self, params: SupportsDataclass | None, depth: int) -> str:
|
|
119
|
+
if not isinstance(params, RecapParams):
|
|
120
|
+
raise PromptRenderError(
|
|
121
|
+
"Recap section requires parameters.",
|
|
122
|
+
dataclass_type=RecapParams,
|
|
123
|
+
)
|
|
124
|
+
heading = "#" * (depth + 2)
|
|
125
|
+
prefix = f"{heading} {self.title}"
|
|
126
|
+
bullets = params.bullets
|
|
127
|
+
bullet_lines = "\n".join(f"- {line}" for line in bullets)
|
|
128
|
+
if bullet_lines:
|
|
129
|
+
return f"{prefix}\n\n{bullet_lines}"
|
|
130
|
+
return prefix
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class DelegationPrompt(Generic[ParentOutputT, DelegationOutputT]): # noqa: UP046
|
|
134
|
+
"""Wrap a rendered parent prompt for subagent delegation."""
|
|
135
|
+
|
|
136
|
+
_parent_output_type: ClassVar[type[Any] | None] = None
|
|
137
|
+
_delegation_output_type: ClassVar[type[Any] | None] = None
|
|
138
|
+
|
|
139
|
+
def __class_getitem__(
|
|
140
|
+
cls, item: tuple[type[Any], type[Any]]
|
|
141
|
+
) -> type["DelegationPrompt[Any, Any]"]: # noqa: UP037
|
|
142
|
+
parent_output, delegation_output = item
|
|
143
|
+
|
|
144
|
+
name = f"{cls.__name__}[{parent_output.__name__}, {delegation_output.__name__}]"
|
|
145
|
+
namespace = {
|
|
146
|
+
"__module__": cls.__module__,
|
|
147
|
+
"_parent_output_type": parent_output,
|
|
148
|
+
"_delegation_output_type": delegation_output,
|
|
149
|
+
}
|
|
150
|
+
specialized = type(name, (cls,), namespace)
|
|
151
|
+
return cast("type[DelegationPrompt[Any, Any]]", specialized)
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
parent_prompt: PromptProtocol[ParentOutputT] | Prompt[ParentOutputT],
|
|
156
|
+
rendered_parent: RenderedPrompt[ParentOutputT],
|
|
157
|
+
*,
|
|
158
|
+
include_response_format: bool = False,
|
|
159
|
+
) -> None:
|
|
160
|
+
super().__init__()
|
|
161
|
+
self._rendered_parent = rendered_parent
|
|
162
|
+
summary_section = DelegationSummarySection()
|
|
163
|
+
parent_section = ParentPromptSection(
|
|
164
|
+
tools=rendered_parent.tools,
|
|
165
|
+
default_params=ParentPromptParams(body=rendered_parent.text),
|
|
166
|
+
)
|
|
167
|
+
sections: list[Section[SupportsDataclass]] = [summary_section]
|
|
168
|
+
|
|
169
|
+
if include_response_format:
|
|
170
|
+
response_section = self._build_response_format_section(rendered_parent)
|
|
171
|
+
if response_section is not None:
|
|
172
|
+
sections.append(response_section)
|
|
173
|
+
|
|
174
|
+
sections.append(parent_section)
|
|
175
|
+
|
|
176
|
+
self._recap_section = RecapSection()
|
|
177
|
+
sections.append(self._recap_section)
|
|
178
|
+
|
|
179
|
+
delegation_output_type = self._resolve_delegation_output_type()
|
|
180
|
+
prompt_cls: type[Prompt[DelegationOutputT]] = Prompt[delegation_output_type]
|
|
181
|
+
self._prompt = prompt_cls(
|
|
182
|
+
ns=f"{parent_prompt.ns}.delegation",
|
|
183
|
+
key=f"{parent_prompt.key}-wrapper",
|
|
184
|
+
sections=tuple(sections),
|
|
185
|
+
inject_output_instructions=False,
|
|
186
|
+
allow_extra_keys=bool(rendered_parent.allow_extra_keys),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def _resolve_delegation_output_type(self) -> type[DelegationOutputT]:
|
|
190
|
+
candidate = getattr(type(self), "_delegation_output_type", None)
|
|
191
|
+
if isinstance(candidate, type):
|
|
192
|
+
return cast(type[DelegationOutputT], candidate)
|
|
193
|
+
|
|
194
|
+
msg = "Specialize DelegationPrompt with an explicit output type"
|
|
195
|
+
raise TypeError(msg)
|
|
196
|
+
|
|
197
|
+
def _build_response_format_section(
|
|
198
|
+
self,
|
|
199
|
+
rendered_parent: RenderedPrompt[ParentOutputT],
|
|
200
|
+
) -> ResponseFormatSection | None:
|
|
201
|
+
container = rendered_parent.container
|
|
202
|
+
if container is None:
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
article = "an" if container.startswith(tuple("aeiou")) else "a"
|
|
206
|
+
extra_clause = (
|
|
207
|
+
"." if rendered_parent.allow_extra_keys else ". Do not add extra keys."
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return ResponseFormatSection(
|
|
211
|
+
params=ResponseFormatParams(
|
|
212
|
+
article=article, container=container, extra_clause=extra_clause
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def prompt(self) -> Prompt[DelegationOutputT]:
|
|
218
|
+
"""Expose the composed prompt for direct access when required."""
|
|
219
|
+
|
|
220
|
+
return self._prompt
|
|
221
|
+
|
|
222
|
+
def render(
|
|
223
|
+
self,
|
|
224
|
+
summary: DelegationParams,
|
|
225
|
+
parent: ParentPromptParams | None = None,
|
|
226
|
+
recap: RecapParams | None = None,
|
|
227
|
+
) -> RenderedPrompt[DelegationOutputT]:
|
|
228
|
+
params: list[SupportsDataclass] = [summary]
|
|
229
|
+
parent_params = parent or ParentPromptParams(body=self._rendered_parent.text)
|
|
230
|
+
params.append(parent_params)
|
|
231
|
+
|
|
232
|
+
default_recap = RecapParams(bullets=tuple(summary.recap_lines))
|
|
233
|
+
recap_params = recap or default_recap
|
|
234
|
+
params.append(recap_params)
|
|
235
|
+
|
|
236
|
+
rendered = self._prompt.render(*tuple(params))
|
|
237
|
+
parent_deadline = self._rendered_parent.deadline
|
|
238
|
+
if parent_deadline is not None and rendered.deadline is not parent_deadline:
|
|
239
|
+
rendered = replace(rendered, deadline=parent_deadline)
|
|
240
|
+
merged_descriptions = _merge_tool_param_descriptions(
|
|
241
|
+
self._rendered_parent.tool_param_descriptions,
|
|
242
|
+
rendered.tool_param_descriptions,
|
|
243
|
+
)
|
|
244
|
+
if merged_descriptions is rendered.tool_param_descriptions:
|
|
245
|
+
return rendered
|
|
246
|
+
return replace(rendered, _tool_param_descriptions=merged_descriptions)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _merge_tool_param_descriptions(
|
|
250
|
+
parent_descriptions: Mapping[str, Mapping[str, str]],
|
|
251
|
+
rendered_descriptions: Mapping[str, Mapping[str, str]],
|
|
252
|
+
) -> Mapping[str, Mapping[str, str]]:
|
|
253
|
+
if not parent_descriptions:
|
|
254
|
+
return rendered_descriptions
|
|
255
|
+
|
|
256
|
+
merged: dict[str, dict[str, str]] = {
|
|
257
|
+
name: dict(fields) for name, fields in parent_descriptions.items()
|
|
258
|
+
}
|
|
259
|
+
for name, fields in rendered_descriptions.items():
|
|
260
|
+
if name not in merged:
|
|
261
|
+
merged[name] = dict(fields)
|
|
262
|
+
else:
|
|
263
|
+
merged[name].update(fields)
|
|
264
|
+
|
|
265
|
+
return MappingProxyType(
|
|
266
|
+
{
|
|
267
|
+
name: MappingProxyType(dict(field_mapping))
|
|
268
|
+
for name, field_mapping in merged.items()
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
__all__ = [
|
|
274
|
+
"DelegationParams",
|
|
275
|
+
"DelegationPrompt",
|
|
276
|
+
"DelegationSummarySection",
|
|
277
|
+
"ParentPromptParams",
|
|
278
|
+
"ParentPromptSection",
|
|
279
|
+
"RecapParams",
|
|
280
|
+
"RecapSection",
|
|
281
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
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 Sequence
|
|
16
|
+
|
|
17
|
+
SectionPath = tuple[str, ...]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _normalize_section_path(section_path: Sequence[str] | None) -> SectionPath:
|
|
21
|
+
if section_path is None:
|
|
22
|
+
return ()
|
|
23
|
+
return tuple(section_path)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PromptError(Exception):
|
|
27
|
+
"""Base class for prompt-related failures providing structured context."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
message: str,
|
|
32
|
+
*,
|
|
33
|
+
section_path: Sequence[str] | None = None,
|
|
34
|
+
dataclass_type: type | None = None,
|
|
35
|
+
placeholder: str | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
super().__init__(message)
|
|
38
|
+
self.message = message
|
|
39
|
+
self.section_path: SectionPath = _normalize_section_path(section_path)
|
|
40
|
+
self.dataclass_type = dataclass_type
|
|
41
|
+
self.placeholder = placeholder
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PromptValidationError(PromptError):
|
|
45
|
+
"""Raised when prompt construction validation fails."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PromptRenderError(PromptError):
|
|
49
|
+
"""Raised when rendering a prompt fails."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"PromptError",
|
|
54
|
+
"PromptRenderError",
|
|
55
|
+
"PromptValidationError",
|
|
56
|
+
"SectionPath",
|
|
57
|
+
]
|