You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

coder_reviewer.py 10 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. """
  2. This example shows how to use publish-subscribe to implement
  3. a simple interaction between a coder and a reviewer agent.
  4. 1. The coder agent receives a code writing task message, generates a code block,
  5. and publishes a code review task message.
  6. 2. The reviewer agent receives the code review task message, reviews the code block,
  7. and publishes a code review result message.
  8. 3. The coder agent receives the code review result message, depending on the result:
  9. if the code is approved, it publishes a code writing result message; otherwise, it generates
  10. a new code block and publishes a code review task message.
  11. 4. The process continues until the coder agent publishes a code writing result message.
  12. """
  13. import asyncio
  14. import json
  15. import os
  16. import re
  17. import sys
  18. import uuid
  19. from dataclasses import dataclass
  20. from typing import Dict, List, Union
  21. from agnext.application import SingleThreadedAgentRuntime
  22. from agnext.components import DefaultSubscription, DefaultTopicId, RoutedAgent, message_handler
  23. from agnext.components.models import (
  24. AssistantMessage,
  25. ChatCompletionClient,
  26. LLMMessage,
  27. SystemMessage,
  28. UserMessage,
  29. )
  30. sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
  31. from agnext.core import MessageContext
  32. from common.utils import get_chat_completion_client_from_envs
  33. @dataclass
  34. class CodeWritingTask:
  35. task: str
  36. @dataclass
  37. class CodeWritingResult:
  38. task: str
  39. code: str
  40. review: str
  41. @dataclass
  42. class CodeReviewTask:
  43. session_id: str
  44. code_writing_task: str
  45. code_writing_scratchpad: str
  46. code: str
  47. @dataclass
  48. class CodeReviewResult:
  49. review: str
  50. session_id: str
  51. approved: bool
  52. class ReviewerAgent(RoutedAgent):
  53. """An agent that performs code review tasks."""
  54. def __init__(
  55. self,
  56. description: str,
  57. model_client: ChatCompletionClient,
  58. ) -> None:
  59. super().__init__(description)
  60. self._system_messages = [
  61. SystemMessage(
  62. content="""You are a code reviewer. You focus on correctness, efficiency and safety of the code.
  63. Respond using the following JSON format:
  64. {
  65. "correctness": "<Your comments>",
  66. "efficiency": "<Your comments>",
  67. "safety": "<Your comments>",
  68. "approval": "<APPROVE or REVISE>",
  69. "suggested_changes": "<Your comments>"
  70. }
  71. """,
  72. )
  73. ]
  74. self._model_client = model_client
  75. @message_handler
  76. async def handle_code_review_task(self, message: CodeReviewTask, ctx: MessageContext) -> None:
  77. # Format the prompt for the code review.
  78. prompt = f"""The problem statement is: {message.code_writing_task}
  79. The code is:
  80. ```
  81. {message.code}
  82. ```
  83. Please review the code and provide feedback.
  84. """
  85. # Generate a response using the chat completion API.
  86. response = await self._model_client.create(
  87. self._system_messages + [UserMessage(content=prompt, source=self.metadata["type"])]
  88. )
  89. assert isinstance(response.content, str)
  90. # TODO: use structured generation library e.g. guidance to ensure the response is in the expected format.
  91. # Parse the response JSON.
  92. review = json.loads(response.content)
  93. # Construct the review text.
  94. review_text = "Code review:\n" + "\n".join([f"{k}: {v}" for k, v in review.items()])
  95. approved = review["approval"].lower().strip() == "approve"
  96. # Publish the review result.
  97. await self.publish_message(
  98. CodeReviewResult(
  99. review=review_text,
  100. approved=approved,
  101. session_id=message.session_id,
  102. ),
  103. topic_id=DefaultTopicId(),
  104. )
  105. class CoderAgent(RoutedAgent):
  106. """An agent that performs code writing tasks."""
  107. def __init__(
  108. self,
  109. description: str,
  110. model_client: ChatCompletionClient,
  111. ) -> None:
  112. super().__init__(
  113. description,
  114. )
  115. self._system_messages = [
  116. SystemMessage(
  117. content="""You are a proficient coder. You write code to solve problems.
  118. Work with the reviewer to improve your code.
  119. Always put all finished code in a single Markdown code block.
  120. For example:
  121. ```python
  122. def hello_world():
  123. print("Hello, World!")
  124. ```
  125. Respond using the following format:
  126. Thoughts: <Your comments>
  127. Code: <Your code>
  128. """,
  129. )
  130. ]
  131. self._model_client = model_client
  132. self._session_memory: Dict[str, List[CodeWritingTask | CodeReviewTask | CodeReviewResult]] = {}
  133. @message_handler
  134. async def handle_code_writing_task(
  135. self,
  136. message: CodeWritingTask,
  137. ctx: MessageContext,
  138. ) -> None:
  139. # Store the messages in a temporary memory for this request only.
  140. session_id = str(uuid.uuid4())
  141. self._session_memory.setdefault(session_id, []).append(message)
  142. # Generate a response using the chat completion API.
  143. response = await self._model_client.create(
  144. self._system_messages + [UserMessage(content=message.task, source=self.metadata["type"])]
  145. )
  146. assert isinstance(response.content, str)
  147. # Extract the code block from the response.
  148. code_block = self._extract_code_block(response.content)
  149. if code_block is None:
  150. raise ValueError("Code block not found.")
  151. # Create a code review task.
  152. code_review_task = CodeReviewTask(
  153. session_id=session_id,
  154. code_writing_task=message.task,
  155. code_writing_scratchpad=response.content,
  156. code=code_block,
  157. )
  158. # Store the code review task in the session memory.
  159. self._session_memory[session_id].append(code_review_task)
  160. # Publish a code review task.
  161. await self.publish_message(
  162. code_review_task,
  163. topic_id=DefaultTopicId(),
  164. )
  165. @message_handler
  166. async def handle_code_review_result(self, message: CodeReviewResult, ctx: MessageContext) -> None:
  167. # Store the review result in the session memory.
  168. self._session_memory[message.session_id].append(message)
  169. # Obtain the request from previous messages.
  170. review_request = next(
  171. m for m in reversed(self._session_memory[message.session_id]) if isinstance(m, CodeReviewTask)
  172. )
  173. assert review_request is not None
  174. # Check if the code is approved.
  175. if message.approved:
  176. # Publish the code writing result.
  177. await self.publish_message(
  178. CodeWritingResult(
  179. code=review_request.code,
  180. task=review_request.code_writing_task,
  181. review=message.review,
  182. ),
  183. topic_id=DefaultTopicId(),
  184. )
  185. print("Code Writing Result:")
  186. print("-" * 80)
  187. print(f"Task:\n{review_request.code_writing_task}")
  188. print("-" * 80)
  189. print(f"Code:\n{review_request.code}")
  190. print("-" * 80)
  191. print(f"Review:\n{message.review}")
  192. print("-" * 80)
  193. else:
  194. # Create a list of LLM messages to send to the model.
  195. messages: List[LLMMessage] = [*self._system_messages]
  196. for m in self._session_memory[message.session_id]:
  197. if isinstance(m, CodeReviewResult):
  198. messages.append(UserMessage(content=m.review, source="Reviewer"))
  199. elif isinstance(m, CodeReviewTask):
  200. messages.append(AssistantMessage(content=m.code_writing_scratchpad, source="Coder"))
  201. elif isinstance(m, CodeWritingTask):
  202. messages.append(UserMessage(content=m.task, source="User"))
  203. else:
  204. raise ValueError(f"Unexpected message type: {m}")
  205. # Generate a revision using the chat completion API.
  206. response = await self._model_client.create(messages)
  207. assert isinstance(response.content, str)
  208. # Extract the code block from the response.
  209. code_block = self._extract_code_block(response.content)
  210. if code_block is None:
  211. raise ValueError("Code block not found.")
  212. # Create a new code review task.
  213. code_review_task = CodeReviewTask(
  214. session_id=message.session_id,
  215. code_writing_task=review_request.code_writing_task,
  216. code_writing_scratchpad=response.content,
  217. code=code_block,
  218. )
  219. # Store the code review task in the session memory.
  220. self._session_memory[message.session_id].append(code_review_task)
  221. # Publish a new code review task.
  222. await self.publish_message(
  223. code_review_task,
  224. topic_id=DefaultTopicId(),
  225. )
  226. def _extract_code_block(self, markdown_text: str) -> Union[str, None]:
  227. pattern = r"```(\w+)\n(.*?)\n```"
  228. # Search for the pattern in the markdown text
  229. match = re.search(pattern, markdown_text, re.DOTALL)
  230. # Extract the language and code block if a match is found
  231. if match:
  232. return match.group(2)
  233. return None
  234. async def main() -> None:
  235. runtime = SingleThreadedAgentRuntime()
  236. await runtime.register(
  237. "ReviewerAgent",
  238. lambda: ReviewerAgent(
  239. description="Code Reviewer",
  240. model_client=get_chat_completion_client_from_envs(model="gpt-4o-mini"),
  241. ),
  242. lambda: [DefaultSubscription()],
  243. )
  244. await runtime.register(
  245. "CoderAgent",
  246. lambda: CoderAgent(
  247. description="Coder",
  248. model_client=get_chat_completion_client_from_envs(model="gpt-4o-mini"),
  249. ),
  250. lambda: [DefaultSubscription()],
  251. )
  252. runtime.start()
  253. await runtime.publish_message(
  254. message=CodeWritingTask(
  255. task="Write a function to find the directory with the largest number of files using multi-processing."
  256. ),
  257. topic_id=DefaultTopicId(),
  258. )
  259. # Keep processing messages until idle.
  260. await runtime.stop_when_idle()
  261. if __name__ == "__main__":
  262. import logging
  263. logging.basicConfig(level=logging.WARNING)
  264. logging.getLogger("agnext").setLevel(logging.DEBUG)
  265. asyncio.run(main())