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

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