Skip to content

Commit d264ec7

Browse files
committed
feat: support interactive feedback
1 parent 3ba6d1c commit d264ec7

File tree

5 files changed

+46
-17
lines changed

5 files changed

+46
-17
lines changed

src/git_draft/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import logging
44

55
from .bots import Action, Bot, Goal
6+
from .feedback import Feedback
67
from .toolbox import Toolbox
78

89

910
__all__ = [
1011
"Action",
1112
"Bot",
13+
"Feedback",
1214
"Goal",
1315
"Toolbox",
1416
]

src/git_draft/bots/common.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pathlib import Path
77

88
from ..common import ensure_state_home, qualified_class_name
9+
from ..feedback import Feedback
910
from ..toolbox import Toolbox
1011

1112

@@ -14,7 +15,14 @@ class Goal:
1415
"""Bot request"""
1516

1617
prompt: str
17-
# TODO: Add timeout.
18+
# TODO: Add timeout here.
19+
20+
21+
class Feedback:
22+
"""teractive user feedback interface"""
23+
24+
def ask(self, question: str) -> str | None:
25+
raise NotImplementedError()
1826

1927

2028
@dataclasses.dataclass
@@ -66,6 +74,8 @@ def state_folder_path(cls, ensure_exists: bool = False) -> Path:
6674
path.mkdir(parents=True, exist_ok=True)
6775
return path
6876

69-
async def act(self, goal: Goal, toolbox: Toolbox) -> Action:
77+
async def act(
78+
self, goal: Goal, toolbox: Toolbox, feedback: Feedback
79+
) -> Action:
7080
"""Runs the bot, striving to achieve the goal with the given toolbox"""
7181
raise NotImplementedError()

src/git_draft/drafter.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from .bots import Action, Bot, Goal
1717
from .common import JSONObject, Progress, qualified_class_name, reindent
18+
from .feedback import Feedback
1819
from .git import SHA, Repo
1920
from .prompt import TemplatedPrompt
2021
from .store import Store, sql
@@ -106,6 +107,7 @@ async def generate_draft(
106107
bot: Bot,
107108
merge_strategy: DraftMergeStrategy | None = None,
108109
prompt_transform: Callable[[str], str] | None = None,
110+
feedback: Feedback | None = None,
109111
) -> Draft:
110112
with self._progress.spinner("Preparing prompt...") as spinner:
111113
# Handle prompt templating and editing. We do this first in case
@@ -146,12 +148,11 @@ async def generate_draft(
146148
change = await self._generate_change(
147149
bot,
148150
Goal(prompt_contents),
149-
toolbox.with_visitors(
150-
[operation_recorder],
151-
),
151+
toolbox.with_visitors([operation_recorder]),
152+
feedback,
152153
)
153154
if change.action.question:
154-
self._progress.report("Requested progress.")
155+
self._progress.report("Requested feedback.")
155156
spinner.update(
156157
"Completed bot run.",
157158
runtime=round(change.walltime.total_seconds(), 1),
@@ -346,12 +347,13 @@ async def _generate_change(
346347
bot: Bot,
347348
goal: Goal,
348349
toolbox: RepoToolbox,
350+
feedback: Feedback,
349351
) -> _Change:
350352
old_tree_sha = toolbox.tree_sha()
351353

352354
start_time = time.perf_counter()
353355
_logger.debug("Running bot... [bot=%s]", bot)
354-
action = await bot.act(goal, toolbox)
356+
action = await bot.act(goal, toolbox, feedback)
355357
_logger.info("Completed bot action. [action=%s]", action)
356358
end_time = time.perf_counter()
357359

src/git_draft/feedback.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""user feedback capabilities"""
2+
3+
4+
class Feedback:
5+
"""Interactive user feedback interface"""
6+
7+
def ask(self, question: str) -> str | None:
8+
raise NotImplementedError()
9+
10+
11+
class LiveFeedback(Feedback):
12+
def ask(self, question: str) -> str | None:
13+
raise NotImplementedError()

src/git_draft/toolbox.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import tempfile
1212
from typing import Protocol, Self, override
1313

14-
from .common import UnreachableError
14+
from .common import UnreachableError, UserFeedback
1515
from .git import SHA, GitError, Repo, null_delimited
1616

1717

@@ -32,10 +32,6 @@ class Toolbox:
3232
# TODO: Support a diff-based edit method.
3333
# https://gist.github.com/noporpoise/16e731849eb1231e86d78f9dfeca3abc
3434

35-
# TODO: Add user feedback tool here. This will make it possible to request
36-
# feedback more than once during a bot action, which leads to a better
37-
# experience when used interactively.
38-
3935
def __init__(self, visitors: Sequence[ToolVisitor] | None = None) -> None:
4036
self._visitors = visitors or []
4137

@@ -133,13 +129,15 @@ def on_delete_file(
133129
) -> None: ... # pragma: no cover
134130

135131
def on_rename_file(
136-
self,
137-
src_path: PurePosixPath,
138-
dst_path: PurePosixPath,
132+
self, src_path: PurePosixPath, dst_path: PurePosixPath
139133
) -> None: ... # pragma: no cover
140134

141135
def on_expose_files(self) -> None: ... # pragma: no cover
142136

137+
def on_request_feedback(
138+
self, question: str
139+
) -> None: ... # pragma: no cover
140+
143141

144142
class NoopToolbox(Toolbox):
145143
"""No-op read-only toolbox"""
@@ -179,18 +177,22 @@ def __init__(
179177
self,
180178
repo: Repo,
181179
start_rev: SHA,
180+
user_feedback: UserFeedback | None = None,
182181
visitors: Sequence[ToolVisitor] | None = None,
183182
) -> None:
184183
super().__init__(visitors)
185184
call = repo.git("rev-parse", "--verify", f"{start_rev}^{{tree}}")
186185
self._tree_sha = call.stdout
187186
self._tree_updates = list[_TreeUpdate]()
188187
self._repo = repo
188+
self._user_feedback = user_feedback
189189

190190
@classmethod
191-
def for_working_dir(cls, repo: Repo) -> tuple[Self, bool]:
191+
def for_working_dir(
192+
cls, repo: Repo, user_feedback: UserFeedback | None = None
193+
) -> tuple[Self, bool]:
192194
index_tree_sha = repo.git("write-tree").stdout
193-
toolbox = cls(repo, index_tree_sha)
195+
toolbox = cls(repo, user_feedback, index_tree_sha)
194196
toolbox._sync_updates() # Apply any changes from the working directory
195197
head_tree_sha = repo.git("rev-parse", "HEAD^{tree}").stdout
196198
return toolbox, toolbox.tree_sha() != head_tree_sha

0 commit comments

Comments
 (0)