Skip to content

Commit 698052a

Browse files
Copilotnotfolder
andauthored
Implement new comment detection and context reflection (#70)
* Initial plan * Add comment detection manager and integrate with task handlers - Create comment_detection_manager.py with CommentDetectionManager class - Add get_comments() abstract method to Task base class - Implement get_comments() for GitHub Issue and PR tasks - Implement get_comments() for GitLab Issue and MR tasks - Integrate CommentDetectionManager into TaskHandler for context storage mode - Integrate CommentDetectionManager into TaskHandler for planning mode - Add _check_and_add_new_comments() method to PlanningCoordinator - Add comment detection state save/restore helpers to TaskHandler - Add comprehensive unit tests for CommentDetectionManager Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * Address code review feedback and format code - Fix comment ID handling to properly check for None vs falsy values - Create dedicated MockGitLabTask class for cleaner test setup - Apply code formatting with ruff Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com>
1 parent 2fcbdb3 commit 698052a

7 files changed

Lines changed: 1000 additions & 0 deletions

comment_detection_manager.py

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
"""新規コメント検知とコンテキスト反映機能.
2+
3+
このモジュールは、Issue/MRの処理中に新しいユーザーコメントを検出し、
4+
LLMコンテキストに反映する機能を提供します。
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import logging
10+
from datetime import datetime, timezone
11+
from typing import TYPE_CHECKING, Any
12+
13+
if TYPE_CHECKING:
14+
from handlers.task import Task
15+
16+
17+
class CommentDetectionManager:
18+
"""Issue/MRの新規コメント検出を管理するクラス.
19+
20+
処理中に追加された新規ユーザーコメントを検出し、
21+
LLMコンテキストに追加する機能を提供します。
22+
"""
23+
24+
def __init__(self, task: Task, config: dict[str, Any]) -> None:
25+
"""CommentDetectionManagerを初期化する.
26+
27+
Args:
28+
task: 処理対象のタスクオブジェクト
29+
config: アプリケーション設定辞書
30+
31+
"""
32+
self.task = task
33+
self.config = config
34+
self.logger = logging.getLogger(__name__)
35+
36+
# コメント検出機能の有効/無効フラグ
37+
self.enabled = False
38+
39+
# 前回までのコメントIDセット(文字列として管理)
40+
self.last_comment_ids: set[str] = set()
41+
42+
# 前回チェック時刻
43+
self.last_check_time: datetime | None = None
44+
45+
# bot自身のユーザー名(除外用)
46+
self.bot_username: str | None = None
47+
48+
# 有効/無効とbot_usernameの決定
49+
self._configure()
50+
51+
def _configure(self) -> None:
52+
"""設定からbot_usernameを取得し、機能の有効/無効を決定する."""
53+
import os
54+
55+
# タスクタイプを取得
56+
task_key = self.task.get_task_key().to_dict()
57+
task_type = task_key.get("type", "")
58+
59+
if task_type.startswith("github"):
60+
# GitHub: 環境変数 > 設定ファイル
61+
self.bot_username = os.environ.get(
62+
"GITHUB_BOT_NAME",
63+
self.config.get("github", {}).get("bot_name"),
64+
)
65+
elif task_type.startswith("gitlab"):
66+
# GitLab: 環境変数 > 設定ファイル
67+
self.bot_username = os.environ.get(
68+
"GITLAB_BOT_NAME",
69+
self.config.get("gitlab", {}).get("bot_name"),
70+
)
71+
else:
72+
self.logger.warning("不明なタスクタイプ: %s", task_type)
73+
self.bot_username = None
74+
75+
# bot_usernameが取得できない場合は機能を無効化
76+
if not self.bot_username:
77+
self.logger.warning("bot_usernameが設定されていません。コメント検出機能を無効化します。")
78+
self.enabled = False
79+
else:
80+
self.enabled = True
81+
self.logger.info(
82+
"コメント検出機能を有効化しました (bot_username=%s)",
83+
self.bot_username,
84+
)
85+
86+
def initialize(self) -> None:
87+
"""現在のコメント一覧を取得してlast_comment_idsを初期化する.
88+
89+
タスク開始時に呼び出して、既存のコメントをすべて記録します。
90+
"""
91+
if not self.enabled:
92+
self.logger.debug("コメント検出機能が無効のため、初期化をスキップします")
93+
return
94+
95+
try:
96+
# 現在のコメント一覧を取得
97+
comments = self.task.get_comments()
98+
99+
# コメントIDをセットに追加(文字列として管理)
100+
self.last_comment_ids = {
101+
str(comment.get("id", "")) for comment in comments if comment.get("id") is not None
102+
}
103+
104+
# チェック時刻を記録
105+
self.last_check_time = datetime.now(timezone.utc)
106+
107+
self.logger.info(
108+
"コメント検出を初期化しました: %d件のコメントを記録",
109+
len(self.last_comment_ids),
110+
)
111+
except Exception as e:
112+
self.logger.warning("コメント取得中にエラー発生: %s", e)
113+
# エラーでも処理を継続
114+
115+
def check_for_new_comments(self) -> list[dict[str, Any]]:
116+
"""新規コメントを検出する.
117+
118+
Returns:
119+
新規コメントのリスト(空リストの場合は新規なし)
120+
121+
"""
122+
if not self.enabled:
123+
return []
124+
125+
try:
126+
# 現在のコメント一覧を取得
127+
current_comments = self.task.get_comments()
128+
129+
# 新規コメントを抽出
130+
new_comments = []
131+
current_ids = set()
132+
133+
for comment in current_comments:
134+
comment_id_raw = comment.get("id")
135+
if comment_id_raw is None:
136+
continue
137+
comment_id = str(comment_id_raw)
138+
139+
current_ids.add(comment_id)
140+
141+
# 新規コメントの判定
142+
if comment_id not in self.last_comment_ids:
143+
# bot自身のコメントを除外
144+
if not self.is_bot_comment(comment):
145+
new_comments.append(comment)
146+
else:
147+
self.logger.debug(
148+
"bot自身のコメントを除外しました: id=%s",
149+
comment_id,
150+
)
151+
152+
# 状態を更新
153+
self.last_comment_ids = current_ids
154+
self.last_check_time = datetime.now(timezone.utc)
155+
156+
if new_comments:
157+
self.logger.info(
158+
"新規コメントを検出しました: %d件 (Task: %s)",
159+
len(new_comments),
160+
getattr(self.task, "uuid", "unknown"),
161+
)
162+
else:
163+
self.logger.debug(
164+
"新規コメントなし (Task: %s)",
165+
getattr(self.task, "uuid", "unknown"),
166+
)
167+
168+
return new_comments
169+
170+
except Exception as e:
171+
self.logger.warning(
172+
"コメント取得中にエラー発生: %s (Task: %s)",
173+
e,
174+
getattr(self.task, "uuid", "unknown"),
175+
)
176+
# エラー時は空リストを返して処理を継続
177+
return []
178+
179+
def is_bot_comment(self, comment: dict[str, Any]) -> bool:
180+
"""コメントがbot自身によるものか判定する.
181+
182+
Args:
183+
comment: コメント情報の辞書
184+
185+
Returns:
186+
botのコメントの場合True
187+
188+
"""
189+
if not self.bot_username:
190+
return False
191+
192+
author = comment.get("author", "")
193+
return author == self.bot_username
194+
195+
def format_comment_message(self, comments: list[dict[str, Any]]) -> str:
196+
"""検出したコメントをLLMメッセージ形式に整形する.
197+
198+
Args:
199+
comments: コメントリスト
200+
201+
Returns:
202+
整形されたメッセージ文字列
203+
204+
"""
205+
if not comments:
206+
return ""
207+
208+
if len(comments) == 1:
209+
# 単一コメントの場合
210+
comment = comments[0]
211+
author = comment.get("author", "unknown")
212+
body = comment.get("body", "")
213+
return f"[New Comment from @{author}]:\n{body}"
214+
215+
# 複数コメントの場合
216+
lines = ["[New Comments Detected]:", ""]
217+
218+
for i, comment in enumerate(comments, 1):
219+
author = comment.get("author", "unknown")
220+
body = comment.get("body", "")
221+
timestamp = comment.get("created_at", "")
222+
223+
lines.append(f"Comment {i} from @{author} ({timestamp}):")
224+
lines.append(body)
225+
lines.append("")
226+
227+
return "\n".join(lines)
228+
229+
def add_to_context(
230+
self,
231+
llm_client: Any, # noqa: ANN401
232+
comments: list[dict[str, Any]],
233+
) -> None:
234+
"""検出したコメントをLLMコンテキストに追加する.
235+
236+
Args:
237+
llm_client: LLMクライアントインスタンス
238+
comments: 追加するコメントのリスト
239+
240+
"""
241+
if not comments:
242+
return
243+
244+
try:
245+
# メッセージを整形
246+
message = self.format_comment_message(comments)
247+
248+
# LLMコンテキストに追加
249+
llm_client.send_user_message(message)
250+
251+
self.logger.info(
252+
"新規コメントをコンテキストに追加しました: %d件 (Task: %s)",
253+
len(comments),
254+
getattr(self.task, "uuid", "unknown"),
255+
)
256+
except Exception as e:
257+
self.logger.warning(
258+
"コンテキスト追加中にエラー発生: %s (Task: %s)",
259+
e,
260+
getattr(self.task, "uuid", "unknown"),
261+
)
262+
263+
def get_state(self) -> dict[str, Any]:
264+
"""一時停止時の状態永続化用に現在の状態を取得する.
265+
266+
Returns:
267+
状態辞書
268+
269+
"""
270+
return {
271+
"last_comment_ids": list(self.last_comment_ids),
272+
"last_check_timestamp": (self.last_check_time.isoformat() if self.last_check_time else None),
273+
}
274+
275+
def restore_state(self, state: dict[str, Any]) -> None:
276+
"""再開時の状態復元用.
277+
278+
Args:
279+
state: get_state()と同じ構造の状態辞書
280+
281+
"""
282+
if not state:
283+
self.logger.debug("復元する状態がありません")
284+
return
285+
286+
try:
287+
# last_comment_idsを復元
288+
comment_ids = state.get("last_comment_ids", [])
289+
self.last_comment_ids = set(comment_ids)
290+
291+
# last_check_timeを復元
292+
timestamp = state.get("last_check_timestamp")
293+
if timestamp:
294+
self.last_check_time = datetime.fromisoformat(timestamp)
295+
296+
self.logger.info(
297+
"コメント検出状態を復元しました: %d件のコメントID",
298+
len(self.last_comment_ids),
299+
)
300+
except Exception as e:
301+
self.logger.warning(
302+
"コメント検出状態の復元に失敗しました: %s。新規初期化を実行します。",
303+
e,
304+
)
305+
# 復元失敗時は新規初期化
306+
self.initialize()

handlers/planning_coordinator.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ def __init__(
103103
# Task stop support
104104
self.stop_manager = None # Will be set by TaskHandler
105105

106+
# Comment detection support
107+
self.comment_detection_manager = None # Will be set by TaskHandler
108+
106109
# Checkbox tracking for progress updates
107110
self.plan_comment_id = None # ID of the comment containing the checklist
108111

@@ -127,6 +130,9 @@ def execute_with_planning(self) -> bool:
127130
self._handle_stop()
128131
return True # Return success to avoid marking as failed
129132

133+
# Check for new comments before starting
134+
self._check_and_add_new_comments()
135+
130136
# Post start comment
131137
self._post_phase_comment("planning", "started", "Beginning task analysis and planning...")
132138

@@ -164,6 +170,9 @@ def execute_with_planning(self) -> bool:
164170
self.logger.info("アサイン解除を検出、タスクを停止します")
165171
self._handle_stop()
166172
return True
173+
174+
# Check for new comments after planning phase
175+
self._check_and_add_new_comments()
167176

168177
# Post execution start
169178
self._post_phase_comment("execution", "started", "Beginning execution of planned actions...")
@@ -187,6 +196,9 @@ def execute_with_planning(self) -> bool:
187196
self._handle_stop()
188197
return True
189198

199+
# Check for new comments before each action
200+
self._check_and_add_new_comments()
201+
190202
# Execute next action
191203
result = self._execute_action()
192204

@@ -219,6 +231,9 @@ def execute_with_planning(self) -> bool:
219231
self._handle_stop()
220232
return True
221233

234+
# Check for new comments before reflection
235+
self._check_and_add_new_comments()
236+
222237
self._post_phase_comment("reflection", "started", f"Analyzing results after {self.action_counter} actions...")
223238
self.current_phase = "reflection"
224239
reflection = self._execute_reflection_phase(result)
@@ -236,6 +251,9 @@ def execute_with_planning(self) -> bool:
236251
self._handle_stop()
237252
return True
238253

254+
# Check for new comments before revision
255+
self._check_and_add_new_comments()
256+
239257
# Revise plan if needed
240258
self._post_phase_comment("revision", "started", "Plan revision needed based on reflection.")
241259
self.current_phase = "revision"
@@ -1063,6 +1081,26 @@ def _handle_stop(self) -> None:
10631081
planning_state=planning_state,
10641082
)
10651083

1084+
def _check_and_add_new_comments(self) -> None:
1085+
"""新規コメントを検出してコンテキストに追加する.
1086+
1087+
comment_detection_managerがNoneの場合は何もしません。
1088+
"""
1089+
if self.comment_detection_manager is None:
1090+
return
1091+
1092+
try:
1093+
new_comments = self.comment_detection_manager.check_for_new_comments()
1094+
if new_comments:
1095+
self.comment_detection_manager.add_to_context(
1096+
self.llm_client, new_comments
1097+
)
1098+
self.logger.info(
1099+
"新規コメント %d件をコンテキストに追加しました", len(new_comments)
1100+
)
1101+
except Exception as e:
1102+
self.logger.warning("新規コメントの検出中にエラー発生: %s", e)
1103+
10661104
def _load_project_agent_rules(self) -> str:
10671105
"""プロジェクト固有のエージェントルールを読み込む.
10681106

0 commit comments

Comments
 (0)