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.
Files changed (73) hide show
  1. weakincentives/__init__.py +67 -0
  2. weakincentives/adapters/__init__.py +37 -0
  3. weakincentives/adapters/_names.py +32 -0
  4. weakincentives/adapters/_provider_protocols.py +69 -0
  5. weakincentives/adapters/_tool_messages.py +80 -0
  6. weakincentives/adapters/core.py +102 -0
  7. weakincentives/adapters/litellm.py +254 -0
  8. weakincentives/adapters/openai.py +254 -0
  9. weakincentives/adapters/shared.py +1021 -0
  10. weakincentives/cli/__init__.py +23 -0
  11. weakincentives/cli/wink.py +58 -0
  12. weakincentives/dbc/__init__.py +412 -0
  13. weakincentives/deadlines.py +58 -0
  14. weakincentives/prompt/__init__.py +105 -0
  15. weakincentives/prompt/_generic_params_specializer.py +64 -0
  16. weakincentives/prompt/_normalization.py +48 -0
  17. weakincentives/prompt/_overrides_protocols.py +33 -0
  18. weakincentives/prompt/_types.py +34 -0
  19. weakincentives/prompt/chapter.py +146 -0
  20. weakincentives/prompt/composition.py +281 -0
  21. weakincentives/prompt/errors.py +57 -0
  22. weakincentives/prompt/markdown.py +108 -0
  23. weakincentives/prompt/overrides/__init__.py +59 -0
  24. weakincentives/prompt/overrides/_fs.py +164 -0
  25. weakincentives/prompt/overrides/inspection.py +141 -0
  26. weakincentives/prompt/overrides/local_store.py +275 -0
  27. weakincentives/prompt/overrides/validation.py +534 -0
  28. weakincentives/prompt/overrides/versioning.py +269 -0
  29. weakincentives/prompt/prompt.py +353 -0
  30. weakincentives/prompt/protocols.py +103 -0
  31. weakincentives/prompt/registry.py +375 -0
  32. weakincentives/prompt/rendering.py +288 -0
  33. weakincentives/prompt/response_format.py +60 -0
  34. weakincentives/prompt/section.py +166 -0
  35. weakincentives/prompt/structured_output.py +179 -0
  36. weakincentives/prompt/tool.py +397 -0
  37. weakincentives/prompt/tool_result.py +30 -0
  38. weakincentives/py.typed +0 -0
  39. weakincentives/runtime/__init__.py +82 -0
  40. weakincentives/runtime/events/__init__.py +126 -0
  41. weakincentives/runtime/events/_types.py +110 -0
  42. weakincentives/runtime/logging.py +284 -0
  43. weakincentives/runtime/session/__init__.py +46 -0
  44. weakincentives/runtime/session/_slice_types.py +24 -0
  45. weakincentives/runtime/session/_types.py +55 -0
  46. weakincentives/runtime/session/dataclasses.py +29 -0
  47. weakincentives/runtime/session/protocols.py +34 -0
  48. weakincentives/runtime/session/reducer_context.py +40 -0
  49. weakincentives/runtime/session/reducers.py +82 -0
  50. weakincentives/runtime/session/selectors.py +56 -0
  51. weakincentives/runtime/session/session.py +387 -0
  52. weakincentives/runtime/session/snapshots.py +310 -0
  53. weakincentives/serde/__init__.py +19 -0
  54. weakincentives/serde/_utils.py +240 -0
  55. weakincentives/serde/dataclass_serde.py +55 -0
  56. weakincentives/serde/dump.py +189 -0
  57. weakincentives/serde/parse.py +417 -0
  58. weakincentives/serde/schema.py +260 -0
  59. weakincentives/tools/__init__.py +154 -0
  60. weakincentives/tools/_context.py +38 -0
  61. weakincentives/tools/asteval.py +853 -0
  62. weakincentives/tools/errors.py +26 -0
  63. weakincentives/tools/planning.py +831 -0
  64. weakincentives/tools/podman.py +1655 -0
  65. weakincentives/tools/subagents.py +346 -0
  66. weakincentives/tools/vfs.py +1390 -0
  67. weakincentives/types/__init__.py +35 -0
  68. weakincentives/types/json.py +45 -0
  69. weakincentives-0.9.0.dist-info/METADATA +775 -0
  70. weakincentives-0.9.0.dist-info/RECORD +73 -0
  71. weakincentives-0.9.0.dist-info/WHEEL +4 -0
  72. weakincentives-0.9.0.dist-info/entry_points.txt +2 -0
  73. weakincentives-0.9.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,108 @@
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 textwrap
16
+ from collections.abc import Callable, Sequence
17
+ from dataclasses import fields, is_dataclass
18
+ from string import Template
19
+ from typing import Any, TypeVar, override
20
+
21
+ from ._types import SupportsDataclass
22
+ from .errors import PromptRenderError
23
+ from .section import Section
24
+
25
+ MarkdownParamsT = TypeVar("MarkdownParamsT", bound=SupportsDataclass, covariant=True)
26
+
27
+
28
+ class MarkdownSection(Section[MarkdownParamsT]):
29
+ """Render markdown content using :class:`string.Template`."""
30
+
31
+ def __init__(
32
+ self,
33
+ *,
34
+ title: str,
35
+ template: str,
36
+ key: str,
37
+ default_params: MarkdownParamsT | None = None,
38
+ children: Sequence[Section[SupportsDataclass]] | None = None,
39
+ enabled: Callable[[SupportsDataclass], bool] | None = None,
40
+ tools: Sequence[object] | None = None,
41
+ accepts_overrides: bool = True,
42
+ ) -> None:
43
+ self.template = template
44
+ super().__init__(
45
+ title=title,
46
+ key=key,
47
+ default_params=default_params,
48
+ children=children,
49
+ enabled=enabled,
50
+ tools=tools,
51
+ accepts_overrides=accepts_overrides,
52
+ )
53
+
54
+ @override
55
+ def render(self, params: SupportsDataclass | None, depth: int) -> str:
56
+ return self.render_with_template(self.template, params, depth)
57
+
58
+ def render_with_template(
59
+ self, template_text: str, params: SupportsDataclass | None, depth: int
60
+ ) -> str:
61
+ heading_level = "#" * (depth + 2)
62
+ heading = f"{heading_level} {self.title.strip()}"
63
+ template = Template(textwrap.dedent(template_text).strip())
64
+ try:
65
+ normalized_params = self._normalize_params(params)
66
+ rendered_body = template.substitute(normalized_params)
67
+ except KeyError as error:
68
+ missing = error.args[0]
69
+ raise PromptRenderError(
70
+ "Missing placeholder during render.",
71
+ placeholder=str(missing),
72
+ ) from error
73
+ if rendered_body:
74
+ return f"{heading}\n\n{rendered_body.strip()}"
75
+ return heading
76
+
77
+ @override
78
+ def placeholder_names(self) -> set[str]:
79
+ template = Template(textwrap.dedent(self.template).strip())
80
+ placeholders: set[str] = set()
81
+ for match in template.pattern.finditer(template.template):
82
+ named = match.group("named")
83
+ if named:
84
+ placeholders.add(named)
85
+ continue
86
+ braced = match.group("braced")
87
+ if braced:
88
+ placeholders.add(braced)
89
+ return placeholders
90
+
91
+ @staticmethod
92
+ def _normalize_params(params: SupportsDataclass | None) -> dict[str, Any]:
93
+ if params is None:
94
+ return {}
95
+ if not is_dataclass(params) or isinstance(params, type):
96
+ raise PromptRenderError(
97
+ "Section params must be a dataclass instance.",
98
+ dataclass_type=type(params),
99
+ )
100
+
101
+ return {field.name: getattr(params, field.name) for field in fields(params)}
102
+
103
+ @override
104
+ def original_body_template(self) -> str:
105
+ return self.template
106
+
107
+
108
+ __all__ = ["MarkdownSection"]
@@ -0,0 +1,59 @@
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
+ """Prompt override infrastructure used by :mod:`weakincentives.prompt`."""
14
+
15
+ from __future__ import annotations
16
+
17
+ from .inspection import (
18
+ OverrideFileMetadata,
19
+ iter_override_files,
20
+ resolve_overrides_root,
21
+ )
22
+ from .local_store import LocalPromptOverridesStore
23
+ from .validation import filter_override_for_descriptor
24
+ from .versioning import (
25
+ HexDigest,
26
+ PromptDescriptor,
27
+ PromptLike,
28
+ PromptOverride,
29
+ PromptOverridesError,
30
+ PromptOverridesStore,
31
+ SectionDescriptor,
32
+ SectionOverride,
33
+ ToolDescriptor,
34
+ ToolOverride,
35
+ ensure_hex_digest,
36
+ hash_json,
37
+ hash_text,
38
+ )
39
+
40
+ __all__ = [
41
+ "HexDigest",
42
+ "LocalPromptOverridesStore",
43
+ "OverrideFileMetadata",
44
+ "PromptDescriptor",
45
+ "PromptLike",
46
+ "PromptOverride",
47
+ "PromptOverridesError",
48
+ "PromptOverridesStore",
49
+ "SectionDescriptor",
50
+ "SectionOverride",
51
+ "ToolDescriptor",
52
+ "ToolOverride",
53
+ "ensure_hex_digest",
54
+ "filter_override_for_descriptor",
55
+ "hash_json",
56
+ "hash_text",
57
+ "iter_override_files",
58
+ "resolve_overrides_root",
59
+ ]
@@ -0,0 +1,164 @@
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
+
18
+ # Bandit false positive: git invocation uses explicit arguments for root
19
+ # discovery.
20
+ import subprocess # nosec B404
21
+ import tempfile
22
+ from collections.abc import Iterator, Mapping
23
+ from contextlib import contextmanager
24
+ from pathlib import Path
25
+ from threading import RLock
26
+ from typing import Any
27
+
28
+ from .versioning import PromptOverridesError
29
+
30
+ _IDENTIFIER_PATTERN = r"^[a-z0-9][a-z0-9._-]{0,63}$"
31
+
32
+
33
+ class OverrideFilesystem:
34
+ """Handle filesystem interactions for prompt overrides."""
35
+
36
+ def __init__(
37
+ self,
38
+ *,
39
+ explicit_root: Path | None,
40
+ overrides_relative_path: Path,
41
+ ) -> None:
42
+ super().__init__()
43
+ self._explicit_root = explicit_root
44
+ self._overrides_relative_path = overrides_relative_path
45
+ self._root_lock = RLock()
46
+ self._root: Path | None = None
47
+ self._path_locks: dict[Path, RLock] = {}
48
+ self._path_locks_lock = RLock()
49
+
50
+ def resolve_root(self) -> Path:
51
+ """Resolve the repository root for overrides operations."""
52
+
53
+ with self._root_lock:
54
+ if self._root is not None:
55
+ return self._root
56
+ if self._explicit_root is not None:
57
+ self._root = self._explicit_root
58
+ return self._root
59
+
60
+ git_root = self._git_toplevel()
61
+ if git_root is not None:
62
+ self._root = git_root
63
+ return self._root
64
+
65
+ traversal_root = self._walk_to_git_root()
66
+ if traversal_root is None:
67
+ raise PromptOverridesError(
68
+ "Failed to locate repository root. Provide root_path explicitly."
69
+ )
70
+ self._root = traversal_root
71
+ return self._root
72
+
73
+ def overrides_dir(self) -> Path:
74
+ return self.resolve_root() / self._overrides_relative_path
75
+
76
+ def override_file_path(
77
+ self,
78
+ *,
79
+ ns: str,
80
+ prompt_key: str,
81
+ tag: str,
82
+ ) -> Path:
83
+ segments = self._split_namespace(ns)
84
+ prompt_component = self.validate_identifier(prompt_key, "prompt key")
85
+ tag_component = self.validate_identifier(tag, "tag")
86
+ directory = self.overrides_dir().joinpath(*segments, prompt_component)
87
+ return directory / f"{tag_component}.json"
88
+
89
+ def validate_identifier(self, value: str, label: str) -> str:
90
+ stripped = value.strip()
91
+ if not stripped:
92
+ raise PromptOverridesError(
93
+ f"{label.capitalize()} must be a non-empty string."
94
+ )
95
+ pattern = _IDENTIFIER_PATTERN
96
+ if not re.fullmatch(pattern, stripped):
97
+ raise PromptOverridesError(
98
+ f"{label.capitalize()} must match pattern {pattern}."
99
+ )
100
+ return stripped
101
+
102
+ @contextmanager
103
+ def locked_override_path(self, file_path: Path) -> Iterator[None]:
104
+ lock = self._get_override_path_lock(file_path)
105
+ with lock:
106
+ yield
107
+
108
+ def atomic_write(self, file_path: Path, payload: Mapping[str, Any]) -> None:
109
+ directory = file_path.parent
110
+ directory.mkdir(parents=True, exist_ok=True)
111
+ with tempfile.NamedTemporaryFile(
112
+ "w", dir=directory, delete=False, encoding="utf-8"
113
+ ) as handle:
114
+ json.dump(payload, handle, indent=2, sort_keys=True)
115
+ _ = handle.write("\n")
116
+ temp_name = Path(handle.name)
117
+ _ = Path(temp_name).replace(file_path)
118
+
119
+ def _get_override_path_lock(self, file_path: Path) -> RLock:
120
+ with self._path_locks_lock:
121
+ lock = self._path_locks.get(file_path)
122
+ if lock is None:
123
+ lock = RLock()
124
+ self._path_locks[file_path] = lock
125
+ return lock
126
+
127
+ def _split_namespace(self, ns: str) -> tuple[str, ...]:
128
+ stripped = ns.strip()
129
+ if not stripped:
130
+ raise PromptOverridesError("Namespace must be a non-empty string.")
131
+ segments = tuple(part.strip() for part in stripped.split("/") if part.strip())
132
+ if not segments:
133
+ raise PromptOverridesError("Namespace must contain at least one segment.")
134
+ return tuple(
135
+ self.validate_identifier(segment, "namespace segment")
136
+ for segment in segments
137
+ )
138
+
139
+ def _git_toplevel(self) -> Path | None:
140
+ try:
141
+ # Bandit false positive: git invocation uses explicit arguments.
142
+ result = subprocess.run( # nosec B603 B607
143
+ ["git", "rev-parse", "--show-toplevel"],
144
+ check=True,
145
+ capture_output=True,
146
+ text=True,
147
+ )
148
+ except (FileNotFoundError, subprocess.CalledProcessError):
149
+ return None
150
+ path = result.stdout.strip()
151
+ if not path:
152
+ return None
153
+ return Path(path).resolve()
154
+
155
+ def _walk_to_git_root(self) -> Path | None:
156
+ current = Path.cwd().resolve()
157
+ for candidate in (current, *current.parents):
158
+ git_dir = candidate / ".git"
159
+ if git_dir.exists():
160
+ return candidate
161
+ return None
162
+
163
+
164
+ __all__ = ["OverrideFilesystem"]
@@ -0,0 +1,141 @@
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
+ """Helpers for inspecting prompt override files on disk."""
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from collections.abc import Iterator
19
+ from dataclasses import dataclass
20
+ from hashlib import sha256
21
+ from pathlib import Path
22
+ from typing import Any, cast
23
+
24
+ from .local_store import LocalPromptOverridesStore
25
+ from .versioning import HexDigest, PromptOverridesError
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class OverrideFileMetadata:
30
+ """Summary information about a prompt override file."""
31
+
32
+ path: Path
33
+ relative_segments: tuple[str, ...]
34
+ modified_time: float
35
+ content_hash: HexDigest
36
+ section_count: int
37
+ tool_count: int
38
+
39
+
40
+ class _InspectableLocalStore(LocalPromptOverridesStore):
41
+ """Expose internal path resolution for read-only inspection helpers."""
42
+
43
+ def overrides_dir(self) -> Path:
44
+ return self._filesystem.overrides_dir()
45
+
46
+
47
+ def resolve_overrides_root(
48
+ *,
49
+ root_path: str | Path | None = None,
50
+ overrides_relative_path: str | Path | None = None,
51
+ ) -> Path:
52
+ """Return the directory that stores local prompt overrides."""
53
+
54
+ if overrides_relative_path is None:
55
+ store = _InspectableLocalStore(root_path=root_path)
56
+ else:
57
+ store = _InspectableLocalStore(
58
+ root_path=root_path,
59
+ overrides_relative_path=overrides_relative_path,
60
+ )
61
+ return store.overrides_dir()
62
+
63
+
64
+ def iter_override_files(
65
+ *,
66
+ overrides_root: Path | None = None,
67
+ root_path: str | Path | None = None,
68
+ overrides_relative_path: str | Path | None = None,
69
+ ) -> Iterator[OverrideFileMetadata]:
70
+ """Yield metadata for every JSON override file on disk."""
71
+
72
+ root = (
73
+ overrides_root
74
+ if overrides_root is not None
75
+ else resolve_overrides_root(
76
+ root_path=root_path, overrides_relative_path=overrides_relative_path
77
+ )
78
+ ).resolve()
79
+
80
+ if not root.exists():
81
+ return
82
+
83
+ for file_path in sorted(root.rglob("*.json")):
84
+ if not file_path.is_file():
85
+ continue
86
+ yield _build_metadata(file_path, root)
87
+
88
+
89
+ def _build_metadata(file_path: Path, overrides_root: Path) -> OverrideFileMetadata:
90
+ try:
91
+ raw = file_path.read_bytes()
92
+ except OSError as error: # pragma: no cover - exercised in practice.
93
+ raise PromptOverridesError(
94
+ f"Failed to read override file: {file_path}"
95
+ ) from error
96
+
97
+ try:
98
+ stat_result = file_path.stat()
99
+ except OSError as error: # pragma: no cover - exercised in practice.
100
+ raise PromptOverridesError(
101
+ f"Failed to stat override file: {file_path}"
102
+ ) from error
103
+
104
+ try:
105
+ payload_obj = json.loads(raw.decode("utf-8"))
106
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
107
+ raise PromptOverridesError(
108
+ f"Failed to parse prompt override JSON: {file_path}"
109
+ ) from error
110
+
111
+ if not isinstance(payload_obj, dict):
112
+ raise PromptOverridesError(
113
+ f"Prompt override payload must be an object: {file_path}"
114
+ )
115
+ payload = cast(dict[str, Any], payload_obj)
116
+
117
+ sections_obj = payload.get("sections", {})
118
+ if not isinstance(sections_obj, dict):
119
+ raise PromptOverridesError(
120
+ f"Override file sections must be a mapping: {file_path}"
121
+ )
122
+ sections = cast(dict[str, Any], sections_obj)
123
+
124
+ tools_obj = payload.get("tools", {})
125
+ if not isinstance(tools_obj, dict):
126
+ raise PromptOverridesError(
127
+ f"Override file tools must be a mapping: {file_path}"
128
+ )
129
+ tools = cast(dict[str, Any], tools_obj)
130
+
131
+ relative_segments = file_path.resolve().relative_to(overrides_root).parts
132
+ content_hash = HexDigest(sha256(raw).hexdigest())
133
+
134
+ return OverrideFileMetadata(
135
+ path=file_path.resolve(),
136
+ relative_segments=relative_segments,
137
+ modified_time=stat_result.st_mtime,
138
+ content_hash=content_hash,
139
+ section_count=len(sections),
140
+ tool_count=len(tools),
141
+ )