1111from langgraph .prebuilt .chat_agent_executor import AgentState
1212from mcp import ClientSession
1313from mcp .client .sse import sse_client
14- from pydantic import Field
14+ from pydantic import BaseModel , Field
1515
1616dotenv .load_dotenv ()
1717
1818
19- class PullRequestState ( AgentState ):
20- """State for the GitHub Assistant agent. """
19+ class PullRequestInfo ( BaseModel ):
20+ """Input parameters with Pull Request details """
2121
2222 owner : str
2323 repo : str
2424 pullNumber : int
25- in_reply_to : Optional [int ]
25+ commentNumber : Optional [int ]
2626 command : str = Field (default = "review" )
2727
2828
29+ class GraphState (AgentState ):
30+ """Graph state"""
31+
32+ owner : str
33+ repo : str
34+ pull_number : int
35+ in_reply_to : Optional [int ]
36+ command : str
37+
38+
2939@asynccontextmanager
3040async def make_graph ():
3141 async with sse_client (
@@ -39,42 +49,38 @@ async def make_graph():
3949 model = ChatAnthropic (model = "claude-3-5-sonnet-latest" )
4050
4151 # Create the conversation history
42- async def hydrate_history (state : PullRequestState ) -> PullRequestState :
52+ async def hydrate_history (input : PullRequestInfo ) -> GraphState :
4353 """Fetch PR context at the start of the workflow."""
44- owner = state ["owner" ]
45- repo = state ["repo" ]
46- pull_number = state ["pullNumber" ]
47- command = state ["command" ]
48- in_reply_to = state ["in_reply_to" ] if "in_reply_to" in state else None
4954
5055 context_messages = []
5156
5257 # Fetch PR details
5358 tool_result = await session .call_tool (
5459 "get_pull_request" ,
5560 {
56- "owner" : owner ,
57- "repo" : repo ,
58- "pullNumber" : pull_number ,
61+ "owner" : input . owner ,
62+ "repo" : input . repo ,
63+ "pullNumber" : input . pullNumber ,
5964 },
6065 )
6166
6267 pr_details = json .loads (tool_result .content [0 ].text )
68+ pr_body = pr_details ["body" ] if "body" in pr_details else ""
6369
6470 # Add PR details as a system message
6571 context_messages .append (
6672 {
6773 "role" : "system" ,
68- "content" : f"Pull Request #{ pull_number } by { pr_details ['user' ]['login' ]} \n Title: { pr_details ['title' ]} \n Description: { pr_details [ 'body' ] } " ,
74+ "content" : f"Pull Request #{ input . pullNumber } by { pr_details ['user' ]['login' ]} \n Title: { pr_details ['title' ]} \n Description: { pr_body } " ,
6975 }
7076 )
7177 # Fetch PR comments
7278 tool_result = await session .call_tool (
7379 "get_pull_request_comments" ,
7480 {
75- "owner" : owner ,
76- "repo" : repo ,
77- "pullNumber" : pull_number ,
81+ "owner" : input . owner ,
82+ "repo" : input . repo ,
83+ "pullNumber" : input . pullNumber ,
7884 },
7985 )
8086
@@ -83,12 +89,12 @@ async def hydrate_history(state: PullRequestState) -> PullRequestState:
8389 # Add each comment as a user or assistant message
8490 for comment in comments :
8591 # Bot comments
86- if comment ["body" ].startswith ("/ai generated " ):
92+ if comment ["body" ].startswith ("_ /ai generated_ \n " ):
8793 context_messages .append (
8894 {
8995 "role" : "assistant" ,
9096 "content" : comment ["body" ]
91- .replace ("/ai generated " , "" )
97+ .replace ("_ /ai generated_ \n " , "" )
9298 .strip (),
9399 }
94100 )
@@ -97,26 +103,27 @@ async def hydrate_history(state: PullRequestState) -> PullRequestState:
97103 context_messages .append (
98104 {"role" : "user" , "content" : comment ["body" ].strip ()}
99105 )
106+
100107 # Fetch PR review comments
101- review_comments = await session .call_tool (
108+ tool_result = await session .call_tool (
102109 "get_pull_request_reviews" ,
103110 {
104- "owner" : owner ,
105- "repo" : repo ,
106- "pullNumber" : pull_number ,
111+ "owner" : input . owner ,
112+ "repo" : input . repo ,
113+ "pullNumber" : input . pullNumber ,
107114 },
108115 )
109116
110117 review_comments = json .loads (tool_result .content [0 ].text )
111118
112119 # Add review comments as messages
113120 for comment in review_comments :
114- if comment ["body" ].startswith ("/ai generated " ):
121+ if comment ["body" ].startswith ("_ /ai generated_ \n " ):
115122 context_messages .append (
116123 {
117124 "role" : "assistant" ,
118125 "content" : comment ["body" ]
119- .replace ("/ai generated " , "" )
126+ .replace ("_ /ai generated_ \n " , "" )
120127 .strip (),
121128 }
122129 )
@@ -127,34 +134,71 @@ async def hydrate_history(state: PullRequestState) -> PullRequestState:
127134 "content" : f"Comment on { comment ['path' ]} line { comment ['line' ]} : { comment ['body' ]} " ,
128135 }
129136 )
130- # Add the input message as the last user message
131- if in_reply_to :
137+
138+ # Fetch PR review comments
139+ tool_result = await session .call_tool (
140+ "get_issue_comments" ,
141+ {
142+ "owner" : input .owner ,
143+ "repo" : input .repo ,
144+ "issue_number" : input .pullNumber ,
145+ "page" : 1 ,
146+ "per_page" : 100
147+ },
148+ )
149+
150+ issue_comments = json .loads (tool_result .content [0 ].text )
151+
152+ # Add review comments as messages
153+ for comment in issue_comments :
154+ if comment ["body" ].startswith ("_/ai generated_\n " ):
132155 context_messages .append (
133156 {
134- "role" : "user" ,
135- "content" : f"The command is { command } with review comment id #{ in_reply_to } " ,
157+ "role" : "assistant" ,
158+ "content" : comment ["body" ]
159+ .replace ("_/ai generated_\n " , "" )
160+ .strip (),
136161 }
137162 )
138163 else :
139164 context_messages .append (
140- {
141- "role" : "user" ,
142- "content" : f"Please { command } this PR and provide detailed feedback." ,
143- }
165+ {"role" : "user" , "content" : comment ["body" ].strip ()}
144166 )
145167
146- # Update the state with the hydrated conversation history
147- return {** state , "messages" : context_messages }
168+ # Add the input message as the last user message
169+ if input .commentNumber :
170+ context_messages .append (
171+ {
172+ "role" : "user" ,
173+ "content" : f"The command is { input .command } with review comment id #{ input .commentNumber } " ,
174+ }
175+ )
176+ else :
177+ context_messages .append (
178+ {
179+ "role" : "user" ,
180+ "content" : f"Please { input .command } this PR and provide detailed feedback." ,
181+ }
182+ )
148183
149- def pr_prompt (state : PullRequestState ):
184+ # Update the state with the hydrated conversation history
185+ return {
186+ "owner" : input .owner ,
187+ "repo" : input .repo ,
188+ "pull_number" : input .pullNumber ,
189+ "in_reply_to" : input .commentNumber ,
190+ "messages" : context_messages ,
191+ }
192+
193+ def pr_prompt (state : GraphState ) -> GraphState :
150194 """Create a prompt that incorporates PR data and the requested command."""
151195 system_message = f"""You are a professional developer with experience in code reviews and GitHub pull requests.
152- You are working with repo: { state ["owner" ]} /{ state ["repo" ]} , PR #{ state ["pullNumber " ]} .
196+ You are working with repo: { state ["owner" ]} /{ state ["repo" ]} , PR #{ state ["pull_number " ]} .
153197
154198 IMPORTANT INSTRUCTIONS:
155199 1. ALWAYS get the contents of the changed files in the current PR
156200 2. ALWAYS use the contents of the changed files as context when replying to a user command.
157- 3. Always start your responses with "/ai generated " to properly tag your comments.
201+ 3. Always start your responses with "_ /ai generated_ \n " to properly tag your comments.
158202 4. When you reply to a comment, make sure to address the specific request.
159203 5. When reviewing code, be thorough but constructive - point out both issues and good practices.
160204 6. You MUST use the available tools to post your response as a PR comment or perform the PR code review.
@@ -185,20 +229,20 @@ def pr_prompt(state: PullRequestState):
185229 # Create the agent node - this will handle both analysis and posting the response
186230 # using its available GitHub tools
187231 agent = create_react_agent (
188- model , tools = tools , state_schema = PullRequestState , prompt = pr_prompt
232+ model , tools = tools , state_schema = GraphState , prompt = pr_prompt
189233 )
190234
191235 # Create a simple two-node StateGraph
192- workflow = StateGraph (PullRequestState )
236+ workflow = StateGraph (GraphState , input = PullRequestInfo )
193237
194238 # Add nodes
195239 workflow .add_node ("hydrate_history" , hydrate_history )
196- workflow .add_node ("agent " , agent )
240+ workflow .add_node ("github_agent " , agent )
197241
198- # Add edges - simple linear flow from context hydration to agent to end
242+ # Add edges - simple linear flow from "history hydration" to "GitHub MCP tools agent" to end
199243 workflow .add_edge ("__start__" , "hydrate_history" )
200- workflow .add_edge ("hydrate_history" , "agent " )
201- workflow .add_edge ("agent " , END )
244+ workflow .add_edge ("hydrate_history" , "github_agent " )
245+ workflow .add_edge ("github_agent " , END )
202246
203247 # Compile the graph
204248 graph = workflow .compile ()
0 commit comments