@@ -23,6 +23,7 @@ def __init__(
2323 base_url : str | None = None ,
2424 agent_label_prefix : str = "agent:" ,
2525 cache_ttl_seconds : int = 300 ,
26+ agent_map : dict [str , str ] | None = None ,
2627 ) -> None :
2728 self ._agent_label_prefix = agent_label_prefix .lower ()
2829 auth = Token (token )
@@ -31,6 +32,7 @@ def __init__(
3132 else :
3233 self ._client = Github (auth = auth )
3334 self ._cache_ttl = max (cache_ttl_seconds , 30 )
35+ self ._agent_map = {k .lower (): v for k , v in (agent_map or {}).items ()}
3436 self ._commit_cache : dict [tuple [str , str ], tuple [float , Optional [Commit .Commit ]]] = {}
3537 self ._label_cache : dict [tuple [str , int ], tuple [float , list [str ]]] = {}
3638 self ._comment_cache : dict [tuple [str , int ], tuple [float , list [str ]]] = {}
@@ -42,17 +44,31 @@ def resolve_agent(
4244 repo_full_name : str ,
4345 pr_number : str | None ,
4446 commit_sha : str | None ,
45- ) -> tuple [Optional [str ], Optional [str ]]:
47+ ) -> tuple [Optional [str ], Optional [str ], dict ]:
4648 agent_id : Optional [str ] = None
4749 session_id : Optional [str ] = None
50+ evidence : dict = {}
4851
4952 if commit_sha :
50- agent_id , session_id = self ._from_commit (repo_full_name , commit_sha )
53+ agent_id , session_id , commit_evidence = self ._from_commit (repo_full_name , commit_sha )
54+ evidence .setdefault ("sources" , []).append (commit_evidence )
5155 if not agent_id and pr_number :
52- agent_id = self ._from_pr_labels (repo_full_name , int (pr_number ))
56+ label_agent , label_evidence = self ._from_pr_labels (repo_full_name , int (pr_number ))
57+ if label_agent :
58+ agent_id = label_agent
59+ evidence .setdefault ("sources" , []).append (label_evidence )
5360 if not agent_id and pr_number :
54- agent_id = self ._from_pr_discussion (repo_full_name , int (pr_number ))
55- return agent_id , session_id
61+ discussion_agent , discussion_evidence = self ._from_pr_discussion (repo_full_name , int (pr_number ))
62+ if discussion_agent :
63+ agent_id = discussion_agent
64+ evidence .setdefault ("sources" , []).append (discussion_evidence )
65+ if not agent_id and pr_number :
66+ body_agent , body_evidence = self ._from_pr_body (repo_full_name , int (pr_number ))
67+ if body_agent :
68+ agent_id = body_agent
69+ evidence .setdefault ("sources" , []).append (body_evidence )
70+ evidence ["agent_id" ] = agent_id
71+ return agent_id , session_id , evidence
5672
5773 def review_stats (self , repo_full_name : str , pr_number : int ) -> dict [str , int ] | None :
5874 comments = self ._fetch_pr_comments (repo_full_name , pr_number )
@@ -82,23 +98,24 @@ def _fetch_commit(self, repo_full_name: str, sha: str) -> Optional[Commit.Commit
8298 self ._commit_cache [key ] = (now + self ._cache_ttl , commit )
8399 return commit
84100
85- def _from_commit (self , repo_full_name : str , sha : str ) -> tuple [Optional [str ], Optional [str ]]:
101+ def _from_commit (self , repo_full_name : str , sha : str ) -> tuple [Optional [str ], Optional [str ], dict ]:
86102 commit = self ._fetch_commit (repo_full_name , sha )
87103 if not commit :
88- return None , None
104+ return None , None , { "source" : "commit" , "reason" : "not_found" }
89105 message = commit .commit .message or ""
90106 for line in message .splitlines ():
91107 match = AGENT_TRAILER_PATTERN .match (line .strip ())
92108 if match :
93- return match .group ("agent" ), None
109+ return match .group ("agent" ), None , { "source" : "commit_trailer" , "line" : line . strip ()}
94110 for line in message .splitlines ():
95111 match = CO_AUTHOR_PATTERN .match (line .strip ())
96112 if match and "copilot" in match .group ("author" ).lower ():
97- return "github-copilot" , None
113+ return "github-copilot" , None , { "source" : "co_author" , "value" : match . group ( "author" )}
98114 author_login = getattr (commit .author , "login" , "" ) or ""
99115 if author_login :
100- return author_login , None
101- return None , None
116+ mapped = self ._agent_map .get (author_login .lower ())
117+ return mapped or author_login , None , {"source" : "commit_author" , "value" : author_login }
118+ return None , None , {"source" : "commit" , "reason" : "no_author" }
102119
103120 def _fetch_pr_labels (self , repo_full_name : str , pr_number : int ) -> list [str ]:
104121 key = (repo_full_name , pr_number )
@@ -115,12 +132,16 @@ def _fetch_pr_labels(self, repo_full_name: str, pr_number: int) -> list[str]:
115132 self ._label_cache [key ] = (now + self ._cache_ttl , labels )
116133 return labels
117134
118- def _from_pr_labels (self , repo_full_name : str , pr_number : int ) -> Optional [str ]:
119- for label in self ._fetch_pr_labels (repo_full_name , pr_number ):
135+ def _from_pr_labels (self , repo_full_name : str , pr_number : int ) -> tuple [Optional [str ], dict ]:
136+ labels = self ._fetch_pr_labels (repo_full_name , pr_number )
137+ for label in labels :
120138 lower = label .lower ()
121139 if lower .startswith (self ._agent_label_prefix ):
122- return label .split (":" , 1 )[- 1 ].strip ()
123- return None
140+ return label .split (":" , 1 )[- 1 ].strip (), {"source" : "label" , "label" : label }
141+ mapped = self ._agent_map .get (lower )
142+ if mapped :
143+ return mapped , {"source" : "label_map" , "label" : label }
144+ return None , {"source" : "label" , "labels" : labels }
124145
125146 def _fetch_pr_comments (self , repo_full_name : str , pr_number : int ) -> list [str ]:
126147 key = (repo_full_name , pr_number )
@@ -168,18 +189,38 @@ def _fetch_review_events(self, repo_full_name: str, pr_number: int) -> int:
168189 self ._review_event_cache [key ] = (now + self ._cache_ttl , events )
169190 return events
170191
171- def _from_pr_discussion (self , repo_full_name : str , pr_number : int ) -> Optional [str ]:
192+ def _from_pr_discussion (self , repo_full_name : str , pr_number : int ) -> tuple [ Optional [str ], dict ]:
172193 for body in self ._fetch_pr_comments (repo_full_name , pr_number ):
173194 for line in body .splitlines ():
174195 match = AGENT_TRAILER_PATTERN .match (line .strip ())
175196 if match :
176- return match .group ("agent" )
197+ return match .group ("agent" ), { "source" : "comment" , "line" : line . strip ()}
177198 for author in self ._fetch_review_authors (repo_full_name , pr_number ):
178199 lower = author .lower ()
179200 if "copilot" in lower :
180- return "github-copilot"
201+ return "github-copilot" , {"source" : "reviewer" , "value" : author }
202+ mapped = self ._agent_map .get (lower )
203+ if mapped :
204+ return mapped , {"source" : "reviewer_map" , "value" : author }
181205 if any (key in lower for key in ("claude" , "gemini" , "gpt" , "bard" )):
182- return lower
206+ return lower , { "source" : "reviewer_heuristic" , "value" : author }
183207 if lower .endswith ("-bot" ):
184- return lower
185- return None
208+ return lower , {"source" : "reviewer_bot" , "value" : author }
209+ return None , {"source" : "discussion" , "reason" : "no_match" }
210+
211+ def _from_pr_body (self , repo_full_name : str , pr_number : int ) -> tuple [Optional [str ], dict ]:
212+ try :
213+ repo = self ._client .get_repo (repo_full_name )
214+ pr = repo .get_pull (pr_number )
215+ body = pr .body or ""
216+ except GithubException :
217+ return None , {"source" : "body" , "reason" : "error" }
218+ for line in body .splitlines ():
219+ match = AGENT_TRAILER_PATTERN .match (line .strip ())
220+ if match :
221+ return match .group ("agent" ), {"source" : "body" , "line" : line .strip ()}
222+ lower_body = body .lower ()
223+ for key , mapped in self ._agent_map .items ():
224+ if key in lower_body :
225+ return mapped , {"source" : "body_map" , "value" : key }
226+ return None , {"source" : "body" , "reason" : "no_match" }
0 commit comments