weakincentives 0.2.0__py3-none-any.whl → 0.3.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.
Potentially problematic release.
This version of weakincentives might be problematic. Click here for more details.
- weakincentives/__init__.py +26 -2
- weakincentives/adapters/__init__.py +6 -5
- weakincentives/adapters/core.py +7 -17
- weakincentives/adapters/litellm.py +594 -0
- weakincentives/adapters/openai.py +286 -57
- weakincentives/events.py +103 -0
- weakincentives/examples/__init__.py +67 -0
- weakincentives/examples/code_review_prompt.py +118 -0
- weakincentives/examples/code_review_session.py +171 -0
- weakincentives/examples/code_review_tools.py +376 -0
- weakincentives/{prompts → prompt}/__init__.py +6 -8
- weakincentives/{prompts → prompt}/_types.py +1 -1
- weakincentives/{prompts/text.py → prompt/markdown.py} +19 -9
- weakincentives/{prompts → prompt}/prompt.py +216 -66
- weakincentives/{prompts → prompt}/response_format.py +9 -6
- weakincentives/{prompts → prompt}/section.py +25 -4
- weakincentives/{prompts/structured.py → prompt/structured_output.py} +16 -5
- weakincentives/{prompts → prompt}/tool.py +6 -6
- weakincentives/prompt/versioning.py +144 -0
- weakincentives/serde/__init__.py +0 -14
- weakincentives/serde/dataclass_serde.py +3 -17
- weakincentives/session/__init__.py +31 -0
- weakincentives/session/reducers.py +60 -0
- weakincentives/session/selectors.py +45 -0
- weakincentives/session/session.py +168 -0
- weakincentives/tools/__init__.py +69 -0
- weakincentives/tools/errors.py +22 -0
- weakincentives/tools/planning.py +538 -0
- weakincentives/tools/vfs.py +590 -0
- weakincentives-0.3.0.dist-info/METADATA +231 -0
- weakincentives-0.3.0.dist-info/RECORD +35 -0
- weakincentives-0.2.0.dist-info/METADATA +0 -173
- weakincentives-0.2.0.dist-info/RECORD +0 -20
- /weakincentives/{prompts → prompt}/errors.py +0 -0
- {weakincentives-0.2.0.dist-info → weakincentives-0.3.0.dist-info}/WHEEL +0 -0
- {weakincentives-0.2.0.dist-info → weakincentives-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
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 scaffolding shared by the code review examples."""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import textwrap
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
|
|
20
|
+
from ..prompt import MarkdownSection, Prompt
|
|
21
|
+
from ..session import Session
|
|
22
|
+
from ..tools import PlanningToolsSection, VfsToolsSection
|
|
23
|
+
from .code_review_tools import build_tools
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ReviewGuidance:
|
|
28
|
+
focus: str = field(
|
|
29
|
+
default=(
|
|
30
|
+
"Identify potential issues, risks, and follow-up questions for the changes "
|
|
31
|
+
"under review."
|
|
32
|
+
),
|
|
33
|
+
metadata={
|
|
34
|
+
"description": "Default framing instructions for the review assistant.",
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ReviewTurnParams:
|
|
41
|
+
request: str = field(
|
|
42
|
+
metadata={
|
|
43
|
+
"description": "User-provided review task or question to address.",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class ReviewResponse:
|
|
50
|
+
summary: str
|
|
51
|
+
issues: list[str]
|
|
52
|
+
next_steps: list[str]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_code_review_prompt(session: Session) -> Prompt[ReviewResponse]:
|
|
56
|
+
tools = build_tools()
|
|
57
|
+
guidance_section = MarkdownSection[ReviewGuidance](
|
|
58
|
+
title="Code Review Brief",
|
|
59
|
+
template=textwrap.dedent(
|
|
60
|
+
"""
|
|
61
|
+
You are a code review assistant working in this repository.
|
|
62
|
+
Every response must stay anchored to the specific task described
|
|
63
|
+
in the review request. If the task is unclear, ask for the missing
|
|
64
|
+
details before proceeding.
|
|
65
|
+
|
|
66
|
+
Use the available tools to stay grounded:
|
|
67
|
+
- `show_git_log` retrieves commit history relevant to the task.
|
|
68
|
+
- `show_git_branches` lists branches that match specified filters.
|
|
69
|
+
- `show_git_tags` lists tags that match specified filters.
|
|
70
|
+
- `show_current_time` reports the present time (default UTC or a
|
|
71
|
+
requested timezone).
|
|
72
|
+
- `vfs_list_directory` lists directories and files staged in the virtual
|
|
73
|
+
filesystem snapshot.
|
|
74
|
+
- `vfs_read_file` reads staged file contents.
|
|
75
|
+
- `vfs_write_file` stages ASCII edits before applying them to the host
|
|
76
|
+
workspace.
|
|
77
|
+
- `vfs_delete_entry` removes staged files or directories that are no
|
|
78
|
+
longer needed.
|
|
79
|
+
If the task requires information beyond these capabilities, ask the
|
|
80
|
+
user for clarification rather than guessing.
|
|
81
|
+
|
|
82
|
+
Maintain a concise working plan for multi-step investigations. Use the
|
|
83
|
+
planning tools to capture the current objective, record step details
|
|
84
|
+
as you gather evidence, and mark tasks complete when finished.
|
|
85
|
+
|
|
86
|
+
Always provide a JSON response with the following keys:
|
|
87
|
+
- summary: Single paragraph capturing the overall state of the changes.
|
|
88
|
+
- issues: List of concrete problems, risks, or follow-up questions tied
|
|
89
|
+
to the task.
|
|
90
|
+
- next_steps: List of actionable recommendations or follow-ups that
|
|
91
|
+
help complete the task or mitigate the issues.
|
|
92
|
+
"""
|
|
93
|
+
).strip(),
|
|
94
|
+
default_params=ReviewGuidance(),
|
|
95
|
+
tools=tools,
|
|
96
|
+
key="code-review-brief",
|
|
97
|
+
)
|
|
98
|
+
planning_section = PlanningToolsSection(session=session)
|
|
99
|
+
vfs_section = VfsToolsSection(session=session)
|
|
100
|
+
user_turn_section = MarkdownSection[ReviewTurnParams](
|
|
101
|
+
title="Review Request",
|
|
102
|
+
template="${request}",
|
|
103
|
+
key="review-request",
|
|
104
|
+
)
|
|
105
|
+
return Prompt[ReviewResponse](
|
|
106
|
+
ns="examples/code-review",
|
|
107
|
+
key="code-review-session",
|
|
108
|
+
name="code_review_agent",
|
|
109
|
+
sections=[guidance_section, planning_section, vfs_section, user_turn_section],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
__all__ = [
|
|
114
|
+
"ReviewGuidance",
|
|
115
|
+
"ReviewTurnParams",
|
|
116
|
+
"ReviewResponse",
|
|
117
|
+
"build_code_review_prompt",
|
|
118
|
+
]
|
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
"""Session orchestration for the code review examples."""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any, Protocol
|
|
20
|
+
|
|
21
|
+
from ..adapters import PromptResponse
|
|
22
|
+
from ..events import EventBus, InProcessEventBus, ToolInvoked
|
|
23
|
+
from ..prompt import Prompt, SupportsDataclass
|
|
24
|
+
from ..serde import dump
|
|
25
|
+
from ..session import (
|
|
26
|
+
DataEvent,
|
|
27
|
+
Session,
|
|
28
|
+
ToolData,
|
|
29
|
+
select_all,
|
|
30
|
+
select_latest,
|
|
31
|
+
)
|
|
32
|
+
from .code_review_prompt import (
|
|
33
|
+
ReviewResponse,
|
|
34
|
+
ReviewTurnParams,
|
|
35
|
+
build_code_review_prompt,
|
|
36
|
+
)
|
|
37
|
+
from .code_review_tools import (
|
|
38
|
+
BranchListResult,
|
|
39
|
+
GitLogResult,
|
|
40
|
+
TagListResult,
|
|
41
|
+
TimeQueryResult,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(slots=True, frozen=True)
|
|
46
|
+
class ToolCallLog:
|
|
47
|
+
"""Recorded tool invocation captured by the session."""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
prompt_name: str
|
|
51
|
+
message: str
|
|
52
|
+
value: dict[str, Any]
|
|
53
|
+
call_id: str | None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SupportsReviewEvaluate(Protocol):
|
|
57
|
+
"""Protocol describing the adapter interface consumed by the session."""
|
|
58
|
+
|
|
59
|
+
def evaluate(
|
|
60
|
+
self,
|
|
61
|
+
prompt: Prompt[ReviewResponse],
|
|
62
|
+
*params: SupportsDataclass,
|
|
63
|
+
parse_output: bool = True,
|
|
64
|
+
bus: EventBus,
|
|
65
|
+
) -> PromptResponse[ReviewResponse]: ...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class CodeReviewSession:
|
|
69
|
+
"""Interactive session wrapper shared by example adapters."""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
adapter: SupportsReviewEvaluate,
|
|
74
|
+
*,
|
|
75
|
+
bus: EventBus | None = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
self._adapter = adapter
|
|
78
|
+
self._bus = bus or InProcessEventBus()
|
|
79
|
+
self._session = Session(bus=self._bus)
|
|
80
|
+
self._prompt = build_code_review_prompt(self._session)
|
|
81
|
+
self._bus.subscribe(ToolInvoked, self._display_tool_event)
|
|
82
|
+
self._register_tool_history()
|
|
83
|
+
|
|
84
|
+
def evaluate(self, request: str) -> str:
|
|
85
|
+
response = self._adapter.evaluate(
|
|
86
|
+
self._prompt,
|
|
87
|
+
ReviewTurnParams(request=request),
|
|
88
|
+
bus=self._bus,
|
|
89
|
+
)
|
|
90
|
+
if response.output is not None:
|
|
91
|
+
rendered_output = dump(response.output, exclude_none=True)
|
|
92
|
+
return json.dumps(
|
|
93
|
+
rendered_output,
|
|
94
|
+
ensure_ascii=False,
|
|
95
|
+
indent=2,
|
|
96
|
+
)
|
|
97
|
+
if response.text:
|
|
98
|
+
return response.text
|
|
99
|
+
return "(no response from assistant)"
|
|
100
|
+
|
|
101
|
+
def render_tool_history(self) -> str:
|
|
102
|
+
history = select_all(self._session, ToolCallLog)
|
|
103
|
+
if not history:
|
|
104
|
+
return "No tool calls recorded yet."
|
|
105
|
+
|
|
106
|
+
lines: list[str] = []
|
|
107
|
+
for index, record in enumerate(history, start=1):
|
|
108
|
+
lines.append(
|
|
109
|
+
f"{index}. {record.name} ({record.prompt_name}) → {record.message}"
|
|
110
|
+
)
|
|
111
|
+
if record.call_id:
|
|
112
|
+
lines.append(f" call_id: {record.call_id}")
|
|
113
|
+
if record.value:
|
|
114
|
+
payload_dump = json.dumps(record.value, ensure_ascii=False)
|
|
115
|
+
lines.append(f" payload: {payload_dump}")
|
|
116
|
+
return "\n".join(lines)
|
|
117
|
+
|
|
118
|
+
def _display_tool_event(self, event: object) -> None:
|
|
119
|
+
if not isinstance(event, ToolInvoked):
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
serialized_params = dump(event.params, exclude_none=True)
|
|
123
|
+
payload = dump(event.result.value, exclude_none=True)
|
|
124
|
+
print(
|
|
125
|
+
f"[tool] {event.name} called with {serialized_params}\n"
|
|
126
|
+
f" → {event.result.message}"
|
|
127
|
+
)
|
|
128
|
+
if payload:
|
|
129
|
+
print(f" payload: {payload}")
|
|
130
|
+
latest = select_latest(self._session, ToolCallLog)
|
|
131
|
+
if latest is not None:
|
|
132
|
+
count = len(select_all(self._session, ToolCallLog))
|
|
133
|
+
print(f" (session recorded this call as #{count})")
|
|
134
|
+
|
|
135
|
+
def _register_tool_history(self) -> None:
|
|
136
|
+
for result_type in (
|
|
137
|
+
GitLogResult,
|
|
138
|
+
TimeQueryResult,
|
|
139
|
+
BranchListResult,
|
|
140
|
+
TagListResult,
|
|
141
|
+
):
|
|
142
|
+
self._session.register_reducer(
|
|
143
|
+
result_type,
|
|
144
|
+
self._record_tool_call,
|
|
145
|
+
slice_type=ToolCallLog,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def _record_tool_call(
|
|
149
|
+
self,
|
|
150
|
+
slice_values: tuple[ToolCallLog, ...],
|
|
151
|
+
event: DataEvent,
|
|
152
|
+
) -> tuple[ToolCallLog, ...]:
|
|
153
|
+
if not isinstance(event, ToolData):
|
|
154
|
+
return slice_values
|
|
155
|
+
|
|
156
|
+
payload = dump(event.value, exclude_none=True)
|
|
157
|
+
record = ToolCallLog(
|
|
158
|
+
name=event.source.name,
|
|
159
|
+
prompt_name=event.source.prompt_name,
|
|
160
|
+
message=event.source.result.message,
|
|
161
|
+
value=payload,
|
|
162
|
+
call_id=event.source.call_id,
|
|
163
|
+
)
|
|
164
|
+
return slice_values + (record,)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
__all__ = [
|
|
168
|
+
"ToolCallLog",
|
|
169
|
+
"SupportsReviewEvaluate",
|
|
170
|
+
"CodeReviewSession",
|
|
171
|
+
]
|
|
@@ -0,0 +1,376 @@
|
|
|
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
|
+
"""Tool metadata and handlers shared by the code review examples."""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import subprocess # nosec B404
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
24
|
+
|
|
25
|
+
from ..prompt import Tool, ToolResult
|
|
26
|
+
|
|
27
|
+
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
28
|
+
MAX_OUTPUT_CHARS = 4000
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _truncate(text: str, max_chars: int = MAX_OUTPUT_CHARS) -> str:
|
|
32
|
+
if len(text) <= max_chars:
|
|
33
|
+
return text
|
|
34
|
+
truncated = text[: max_chars - 20]
|
|
35
|
+
return f"{truncated}\n... (truncated {len(text) - len(truncated)} characters)"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _run_git_command(args: Sequence[str]) -> subprocess.CompletedProcess[str]:
|
|
39
|
+
"""Execute a git command rooted at the repository directory."""
|
|
40
|
+
# Tool handlers build git commands from validated dataclass inputs.
|
|
41
|
+
return subprocess.run( # nosec B603
|
|
42
|
+
list(args),
|
|
43
|
+
cwd=REPO_ROOT,
|
|
44
|
+
check=False,
|
|
45
|
+
capture_output=True,
|
|
46
|
+
text=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class GitLogParams:
|
|
52
|
+
"""Parameters for querying git log history."""
|
|
53
|
+
|
|
54
|
+
revision_range: str | None = field(
|
|
55
|
+
default=None,
|
|
56
|
+
metadata={
|
|
57
|
+
"description": (
|
|
58
|
+
"Commit range spec passed to `git log` (for example 'main..HEAD')."
|
|
59
|
+
)
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
path: str | None = field(
|
|
63
|
+
default=None,
|
|
64
|
+
metadata={
|
|
65
|
+
"description": "Restrict history to a specific file or directory.",
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
max_count: int | None = field(
|
|
69
|
+
default=20,
|
|
70
|
+
metadata={"description": "Maximum number of commits to return."},
|
|
71
|
+
)
|
|
72
|
+
skip: int | None = field(
|
|
73
|
+
default=None,
|
|
74
|
+
metadata={
|
|
75
|
+
"description": "Number of commits to skip from the top of the result.",
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
author: str | None = field(
|
|
79
|
+
default=None,
|
|
80
|
+
metadata={
|
|
81
|
+
"description": ("Filter commits to those authored by the provided string."),
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
since: str | None = field(
|
|
85
|
+
default=None,
|
|
86
|
+
metadata={
|
|
87
|
+
"description": (
|
|
88
|
+
"Only include commits after this date/time (forwarded to git)."
|
|
89
|
+
),
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
until: str | None = field(
|
|
93
|
+
default=None,
|
|
94
|
+
metadata={
|
|
95
|
+
"description": (
|
|
96
|
+
"Only include commits up to this date/time (forwarded to git)."
|
|
97
|
+
),
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
grep: str | None = field(
|
|
101
|
+
default=None,
|
|
102
|
+
metadata={
|
|
103
|
+
"description": (
|
|
104
|
+
"Include commits whose messages match this regular expression."
|
|
105
|
+
),
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
additional_args: tuple[str, ...] = field(
|
|
109
|
+
default_factory=tuple,
|
|
110
|
+
metadata={
|
|
111
|
+
"description": "Extra raw arguments forwarded to `git log`.",
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class GitLogResult:
|
|
118
|
+
entries: list[str]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class TimeQueryParams:
|
|
123
|
+
"""Parameters for requesting the current time."""
|
|
124
|
+
|
|
125
|
+
timezone: str | None = field(
|
|
126
|
+
default=None,
|
|
127
|
+
metadata={
|
|
128
|
+
"description": (
|
|
129
|
+
"IANA timezone identifier to convert the current time. Defaults to UTC."
|
|
130
|
+
),
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class TimeQueryResult:
|
|
137
|
+
iso_timestamp: str
|
|
138
|
+
timezone: str
|
|
139
|
+
source: str
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class BranchListParams:
|
|
144
|
+
"""Parameters for listing git branches."""
|
|
145
|
+
|
|
146
|
+
include_remote: bool = field(
|
|
147
|
+
default=False,
|
|
148
|
+
metadata={
|
|
149
|
+
"description": "Include remote branches when set to true (uses --all).",
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
pattern: str | None = field(
|
|
153
|
+
default=None,
|
|
154
|
+
metadata={
|
|
155
|
+
"description": "Optional glob to filter branch names (passed to git).",
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
contains: str | None = field(
|
|
159
|
+
default=None,
|
|
160
|
+
metadata={
|
|
161
|
+
"description": "Only branches containing this commit (uses --contains).",
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class BranchListResult:
|
|
168
|
+
branches: list[str]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class TagListParams:
|
|
173
|
+
"""Parameters for listing git tags."""
|
|
174
|
+
|
|
175
|
+
pattern: str | None = field(
|
|
176
|
+
default=None,
|
|
177
|
+
metadata={
|
|
178
|
+
"description": "Optional glob to filter tags (passed to git).",
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
sort: str | None = field(
|
|
182
|
+
default=None,
|
|
183
|
+
metadata={
|
|
184
|
+
"description": "Sort directive forwarded to git tag --sort (for example '-version:refname').",
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
contains: str | None = field(
|
|
188
|
+
default=None,
|
|
189
|
+
metadata={
|
|
190
|
+
"description": "Only tags containing this commit (uses --contains).",
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass
|
|
196
|
+
class TagListResult:
|
|
197
|
+
tags: list[str]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def git_log_handler(params: GitLogParams) -> ToolResult[GitLogResult]:
|
|
201
|
+
max_count = params.max_count
|
|
202
|
+
args = ["git", "log", "--oneline"]
|
|
203
|
+
if max_count is not None:
|
|
204
|
+
max_count = max(1, max_count)
|
|
205
|
+
args.append(f"-n{max_count}")
|
|
206
|
+
if params.skip:
|
|
207
|
+
args.extend(["--skip", str(max(params.skip, 0))])
|
|
208
|
+
if params.author:
|
|
209
|
+
args.extend(["--author", params.author])
|
|
210
|
+
if params.since:
|
|
211
|
+
args.extend(["--since", params.since])
|
|
212
|
+
if params.until:
|
|
213
|
+
args.extend(["--until", params.until])
|
|
214
|
+
if params.grep:
|
|
215
|
+
args.extend(["--grep", params.grep])
|
|
216
|
+
if params.additional_args:
|
|
217
|
+
args.extend(params.additional_args)
|
|
218
|
+
if params.revision_range:
|
|
219
|
+
args.append(params.revision_range)
|
|
220
|
+
if params.path:
|
|
221
|
+
args.extend(["--", params.path])
|
|
222
|
+
|
|
223
|
+
result = _run_git_command(args)
|
|
224
|
+
if result.returncode != 0:
|
|
225
|
+
message = f"git log failed: {result.stderr.strip()}"
|
|
226
|
+
return ToolResult(message=message, value=GitLogResult(entries=[]))
|
|
227
|
+
|
|
228
|
+
entries = [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
229
|
+
if not entries:
|
|
230
|
+
message = "No git log entries matched the query."
|
|
231
|
+
else:
|
|
232
|
+
joined_entries = "\n".join(entries)
|
|
233
|
+
message = (
|
|
234
|
+
f"Returned {len(entries)} git log entr{'y' if len(entries) == 1 else 'ies'}:\n"
|
|
235
|
+
f"{_truncate(joined_entries)}"
|
|
236
|
+
)
|
|
237
|
+
return ToolResult(message=message, value=GitLogResult(entries=entries))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def current_time_handler(params: TimeQueryParams) -> ToolResult[TimeQueryResult]:
|
|
241
|
+
requested_timezone = params.timezone or "UTC"
|
|
242
|
+
timezone_name = requested_timezone
|
|
243
|
+
try:
|
|
244
|
+
tzinfo = ZoneInfo(requested_timezone)
|
|
245
|
+
source = "zoneinfo"
|
|
246
|
+
except ZoneInfoNotFoundError:
|
|
247
|
+
tzinfo = ZoneInfo("UTC")
|
|
248
|
+
timezone_name = "UTC"
|
|
249
|
+
source = "fallback"
|
|
250
|
+
|
|
251
|
+
now = datetime.now(tzinfo)
|
|
252
|
+
iso_timestamp = now.isoformat()
|
|
253
|
+
if source == "fallback" and requested_timezone != "UTC":
|
|
254
|
+
message = (
|
|
255
|
+
f"Timezone '{requested_timezone}' not found. "
|
|
256
|
+
f"Using UTC instead.\nCurrent time (UTC): {iso_timestamp}"
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
message = f"Current time in {timezone_name}: {iso_timestamp}"
|
|
260
|
+
|
|
261
|
+
return ToolResult(
|
|
262
|
+
message=message,
|
|
263
|
+
value=TimeQueryResult(
|
|
264
|
+
iso_timestamp=iso_timestamp,
|
|
265
|
+
timezone=timezone_name,
|
|
266
|
+
source=source,
|
|
267
|
+
),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def branch_list_handler(params: BranchListParams) -> ToolResult[BranchListResult]:
|
|
272
|
+
args = ["git", "branch", "--list"]
|
|
273
|
+
if params.include_remote:
|
|
274
|
+
args.append("--all")
|
|
275
|
+
if params.contains:
|
|
276
|
+
args.extend(["--contains", params.contains])
|
|
277
|
+
if params.pattern:
|
|
278
|
+
args.append(params.pattern)
|
|
279
|
+
|
|
280
|
+
result = _run_git_command(args)
|
|
281
|
+
if result.returncode != 0:
|
|
282
|
+
message = f"git branch failed: {result.stderr.strip()}"
|
|
283
|
+
return ToolResult(message=message, value=BranchListResult(branches=[]))
|
|
284
|
+
|
|
285
|
+
branches = [line.strip().lstrip("* ") for line in result.stdout.splitlines()]
|
|
286
|
+
branches = [branch for branch in branches if branch]
|
|
287
|
+
if not branches:
|
|
288
|
+
message = "No branches matched the query."
|
|
289
|
+
else:
|
|
290
|
+
message = (
|
|
291
|
+
f"Returned {len(branches)} branch entr{'y' if len(branches) == 1 else 'ies'}:\n"
|
|
292
|
+
f"{_truncate('\n'.join(branches))}"
|
|
293
|
+
)
|
|
294
|
+
return ToolResult(message=message, value=BranchListResult(branches=branches))
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def tag_list_handler(params: TagListParams) -> ToolResult[TagListResult]:
|
|
298
|
+
args = ["git", "tag", "--list"]
|
|
299
|
+
if params.contains:
|
|
300
|
+
args.extend(["--contains", params.contains])
|
|
301
|
+
if params.sort:
|
|
302
|
+
args.extend(["--sort", params.sort])
|
|
303
|
+
if params.pattern:
|
|
304
|
+
args.append(params.pattern)
|
|
305
|
+
|
|
306
|
+
result = _run_git_command(args)
|
|
307
|
+
if result.returncode != 0:
|
|
308
|
+
message = f"git tag failed: {result.stderr.strip()}"
|
|
309
|
+
return ToolResult(message=message, value=TagListResult(tags=[]))
|
|
310
|
+
|
|
311
|
+
tags = [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
312
|
+
if not tags:
|
|
313
|
+
message = "No tags matched the query."
|
|
314
|
+
else:
|
|
315
|
+
message = (
|
|
316
|
+
f"Returned {len(tags)} tag entr{'y' if len(tags) == 1 else 'ies'}:\n"
|
|
317
|
+
f"{_truncate('\n'.join(tags))}"
|
|
318
|
+
)
|
|
319
|
+
return ToolResult(message=message, value=TagListResult(tags=tags))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def build_tools() -> tuple[Tool[Any, Any], ...]:
|
|
323
|
+
git_log_tool = Tool[GitLogParams, GitLogResult](
|
|
324
|
+
name="show_git_log",
|
|
325
|
+
description=(
|
|
326
|
+
"Inspect repository history using git log filters such as revision "
|
|
327
|
+
"ranges, authors, dates, grep patterns, and file paths."
|
|
328
|
+
),
|
|
329
|
+
handler=git_log_handler,
|
|
330
|
+
)
|
|
331
|
+
current_time_tool = Tool[TimeQueryParams, TimeQueryResult](
|
|
332
|
+
name="show_current_time",
|
|
333
|
+
description="Fetch the current time in UTC or a provided timezone using zoneinfo.",
|
|
334
|
+
handler=current_time_handler,
|
|
335
|
+
)
|
|
336
|
+
branch_list_tool = Tool[BranchListParams, BranchListResult](
|
|
337
|
+
name="show_git_branches",
|
|
338
|
+
description=(
|
|
339
|
+
"List local or remote branches with optional glob filters and commit containment checks."
|
|
340
|
+
),
|
|
341
|
+
handler=branch_list_handler,
|
|
342
|
+
)
|
|
343
|
+
tag_list_tool = Tool[TagListParams, TagListResult](
|
|
344
|
+
name="show_git_tags",
|
|
345
|
+
description=(
|
|
346
|
+
"List repository tags with optional glob filters, sorting, and commit containment checks."
|
|
347
|
+
),
|
|
348
|
+
handler=tag_list_handler,
|
|
349
|
+
)
|
|
350
|
+
return (
|
|
351
|
+
git_log_tool,
|
|
352
|
+
current_time_tool,
|
|
353
|
+
branch_list_tool,
|
|
354
|
+
tag_list_tool,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
__all__ = [
|
|
359
|
+
"REPO_ROOT",
|
|
360
|
+
"MAX_OUTPUT_CHARS",
|
|
361
|
+
"GitLogParams",
|
|
362
|
+
"GitLogResult",
|
|
363
|
+
"TimeQueryParams",
|
|
364
|
+
"TimeQueryResult",
|
|
365
|
+
"BranchListParams",
|
|
366
|
+
"BranchListResult",
|
|
367
|
+
"TagListParams",
|
|
368
|
+
"TagListResult",
|
|
369
|
+
"git_log_handler",
|
|
370
|
+
"current_time_handler",
|
|
371
|
+
"branch_list_handler",
|
|
372
|
+
"tag_list_handler",
|
|
373
|
+
"build_tools",
|
|
374
|
+
"_run_git_command",
|
|
375
|
+
"_truncate",
|
|
376
|
+
]
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# See the License for the specific language governing permissions and
|
|
11
11
|
# limitations under the License.
|
|
12
12
|
|
|
13
|
-
"""Prompt
|
|
13
|
+
"""Prompt authoring primitives exposed by :mod:`weakincentives.prompt`."""
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
@@ -21,25 +21,23 @@ from .errors import (
|
|
|
21
21
|
PromptValidationError,
|
|
22
22
|
SectionPath,
|
|
23
23
|
)
|
|
24
|
-
from .
|
|
24
|
+
from .markdown import MarkdownSection
|
|
25
|
+
from .prompt import Prompt
|
|
25
26
|
from .section import Section
|
|
26
|
-
from .
|
|
27
|
-
from .text import TextSection
|
|
27
|
+
from .structured_output import OutputParseError, parse_structured_output
|
|
28
28
|
from .tool import Tool, ToolResult
|
|
29
29
|
|
|
30
30
|
__all__ = [
|
|
31
31
|
"Prompt",
|
|
32
|
-
"RenderedPrompt",
|
|
33
|
-
"PromptSectionNode",
|
|
34
32
|
"PromptError",
|
|
35
33
|
"PromptRenderError",
|
|
36
34
|
"PromptValidationError",
|
|
37
35
|
"Section",
|
|
38
36
|
"SectionPath",
|
|
39
37
|
"SupportsDataclass",
|
|
40
|
-
"
|
|
38
|
+
"MarkdownSection",
|
|
41
39
|
"Tool",
|
|
42
40
|
"ToolResult",
|
|
43
41
|
"OutputParseError",
|
|
44
|
-
"
|
|
42
|
+
"parse_structured_output",
|
|
45
43
|
]
|