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.

main.py 10 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. """This is an example of simulating a chess game with two agents
  2. that play against each other, using tools to reason about the game state
  3. and make moves. The agents subscribe to the default topic and publish their
  4. moves to the default topic."""
  5. import argparse
  6. import asyncio
  7. import logging
  8. import yaml
  9. from typing import Annotated, Any, Dict, List, Literal
  10. from autogen_core import (
  11. AgentId,
  12. AgentRuntime,
  13. DefaultTopicId,
  14. MessageContext,
  15. RoutedAgent,
  16. SingleThreadedAgentRuntime,
  17. default_subscription,
  18. message_handler,
  19. )
  20. from autogen_core.model_context import BufferedChatCompletionContext, ChatCompletionContext
  21. from autogen_core.models import (
  22. ChatCompletionClient,
  23. LLMMessage,
  24. SystemMessage,
  25. UserMessage,
  26. )
  27. from autogen_core.tool_agent import ToolAgent, tool_agent_caller_loop
  28. from autogen_core.tools import FunctionTool, Tool, ToolSchema
  29. from chess import BLACK, SQUARE_NAMES, WHITE, Board, Move
  30. from chess import piece_name as get_piece_name
  31. from pydantic import BaseModel
  32. class TextMessage(BaseModel):
  33. source: str
  34. content: str
  35. @default_subscription
  36. class PlayerAgent(RoutedAgent):
  37. def __init__(
  38. self,
  39. description: str,
  40. instructions: str,
  41. model_client: ChatCompletionClient,
  42. model_context: ChatCompletionContext,
  43. tool_schema: List[ToolSchema],
  44. tool_agent_type: str,
  45. ) -> None:
  46. super().__init__(description=description)
  47. self._system_messages: List[LLMMessage] = [SystemMessage(content=instructions)]
  48. self._model_client = model_client
  49. self._tool_schema = tool_schema
  50. self._tool_agent_id = AgentId(tool_agent_type, self.id.key)
  51. self._model_context = model_context
  52. @message_handler
  53. async def handle_message(self, message: TextMessage, ctx: MessageContext) -> None:
  54. # Add the user message to the model context.
  55. await self._model_context.add_message(UserMessage(content=message.content, source=message.source))
  56. # Run the caller loop to handle tool calls.
  57. messages = await tool_agent_caller_loop(
  58. self,
  59. tool_agent_id=self._tool_agent_id,
  60. model_client=self._model_client,
  61. input_messages=self._system_messages + (await self._model_context.get_messages()),
  62. tool_schema=self._tool_schema,
  63. cancellation_token=ctx.cancellation_token,
  64. )
  65. # Add the assistant message to the model context.
  66. for msg in messages:
  67. await self._model_context.add_message(msg)
  68. # Publish the final response.
  69. assert isinstance(messages[-1].content, str)
  70. await self.publish_message(TextMessage(content=messages[-1].content, source=self.id.type), DefaultTopicId())
  71. def validate_turn(board: Board, player: Literal["white", "black"]) -> None:
  72. """Validate that it is the player's turn to move."""
  73. last_move = board.peek() if board.move_stack else None
  74. if last_move is not None:
  75. if player == "white" and board.color_at(last_move.to_square) == WHITE:
  76. raise ValueError("It is not your turn to move. Wait for black to move.")
  77. if player == "black" and board.color_at(last_move.to_square) == BLACK:
  78. raise ValueError("It is not your turn to move. Wait for white to move.")
  79. elif last_move is None and player != "white":
  80. raise ValueError("It is not your turn to move. Wait for white to move first.")
  81. def get_legal_moves(
  82. board: Board, player: Literal["white", "black"]
  83. ) -> Annotated[str, "A list of legal moves in UCI format."]:
  84. """Get legal moves for the given player."""
  85. validate_turn(board, player)
  86. legal_moves = list(board.legal_moves)
  87. if player == "black":
  88. legal_moves = [move for move in legal_moves if board.color_at(move.from_square) == BLACK]
  89. elif player == "white":
  90. legal_moves = [move for move in legal_moves if board.color_at(move.from_square) == WHITE]
  91. else:
  92. raise ValueError("Invalid player, must be either 'black' or 'white'.")
  93. if not legal_moves:
  94. return "No legal moves. The game is over."
  95. return "Possible moves are: " + ", ".join([move.uci() for move in legal_moves])
  96. def get_board(board: Board) -> str:
  97. """Get the current board state."""
  98. return str(board)
  99. def make_move(
  100. board: Board,
  101. player: Literal["white", "black"],
  102. thinking: Annotated[str, "Thinking for the move."],
  103. move: Annotated[str, "A move in UCI format."],
  104. ) -> Annotated[str, "Result of the move."]:
  105. """Make a move on the board."""
  106. validate_turn(board, player)
  107. new_move = Move.from_uci(move)
  108. board.push(new_move)
  109. # Print the move.
  110. print("-" * 50)
  111. print("Player:", player)
  112. print("Move:", new_move.uci())
  113. print("Thinking:", thinking)
  114. print("Board:")
  115. print(board.unicode(borders=True))
  116. # Get the piece name.
  117. piece = board.piece_at(new_move.to_square)
  118. assert piece is not None
  119. piece_symbol = piece.unicode_symbol()
  120. piece_name = get_piece_name(piece.piece_type)
  121. if piece_symbol.isupper():
  122. piece_name = piece_name.capitalize()
  123. return f"Moved {piece_name} ({piece_symbol}) from {SQUARE_NAMES[new_move.from_square]} to {SQUARE_NAMES[new_move.to_square]}."
  124. async def chess_game(runtime: AgentRuntime, model_client : ChatCompletionClient) -> None: # type: ignore
  125. """Create agents for a chess game and return the group chat."""
  126. # Create the board.
  127. board = Board()
  128. # Create tools for each player.
  129. def get_legal_moves_black() -> str:
  130. return get_legal_moves(board, "black")
  131. def get_legal_moves_white() -> str:
  132. return get_legal_moves(board, "white")
  133. def make_move_black(
  134. thinking: Annotated[str, "Thinking for the move"],
  135. move: Annotated[str, "A move in UCI format"],
  136. ) -> str:
  137. return make_move(board, "black", thinking, move)
  138. def make_move_white(
  139. thinking: Annotated[str, "Thinking for the move"],
  140. move: Annotated[str, "A move in UCI format"],
  141. ) -> str:
  142. return make_move(board, "white", thinking, move)
  143. def get_board_text() -> Annotated[str, "The current board state"]:
  144. return get_board(board)
  145. black_tools: List[Tool] = [
  146. FunctionTool(
  147. get_legal_moves_black,
  148. name="get_legal_moves",
  149. description="Get legal moves.",
  150. ),
  151. FunctionTool(
  152. make_move_black,
  153. name="make_move",
  154. description="Make a move.",
  155. ),
  156. FunctionTool(
  157. get_board_text,
  158. name="get_board",
  159. description="Get the current board state.",
  160. ),
  161. ]
  162. white_tools: List[Tool] = [
  163. FunctionTool(
  164. get_legal_moves_white,
  165. name="get_legal_moves",
  166. description="Get legal moves.",
  167. ),
  168. FunctionTool(
  169. make_move_white,
  170. name="make_move",
  171. description="Make a move.",
  172. ),
  173. FunctionTool(
  174. get_board_text,
  175. name="get_board",
  176. description="Get the current board state.",
  177. ),
  178. ]
  179. # Register the agents.
  180. await ToolAgent.register(
  181. runtime,
  182. "PlayerBlackToolAgent",
  183. lambda: ToolAgent(description="Tool agent for chess game.", tools=black_tools),
  184. )
  185. await ToolAgent.register(
  186. runtime,
  187. "PlayerWhiteToolAgent",
  188. lambda: ToolAgent(description="Tool agent for chess game.", tools=white_tools),
  189. )
  190. await PlayerAgent.register(
  191. runtime,
  192. "PlayerBlack",
  193. lambda: PlayerAgent(
  194. description="Player playing black.",
  195. instructions="You are a chess player and you play as black. Use the tool 'get_board' and 'get_legal_moves' to get the legal moves and 'make_move' to make a move.",
  196. model_client=model_client,
  197. model_context=BufferedChatCompletionContext(buffer_size=10),
  198. tool_schema=[tool.schema for tool in black_tools],
  199. tool_agent_type="PlayerBlackToolAgent",
  200. ),
  201. )
  202. await PlayerAgent.register(
  203. runtime,
  204. "PlayerWhite",
  205. lambda: PlayerAgent(
  206. description="Player playing white.",
  207. instructions="You are a chess player and you play as white. Use the tool 'get_board' and 'get_legal_moves' to get the legal moves and 'make_move' to make a move.",
  208. model_client=model_client,
  209. model_context=BufferedChatCompletionContext(buffer_size=10),
  210. tool_schema=[tool.schema for tool in white_tools],
  211. tool_agent_type="PlayerWhiteToolAgent",
  212. ),
  213. )
  214. async def main(model_config: Dict[str, Any]) -> None:
  215. """Main Entrypoint."""
  216. runtime = SingleThreadedAgentRuntime()
  217. model_client = ChatCompletionClient.load_component(model_config)
  218. await chess_game(runtime, model_client)
  219. runtime.start()
  220. # Publish an initial message to trigger the group chat manager to start
  221. # orchestration.
  222. # Send an initial message to player white to start the game.
  223. await runtime.send_message(
  224. TextMessage(content="Game started, white player your move.", source="System"),
  225. AgentId("PlayerWhite", "default"),
  226. )
  227. await runtime.stop_when_idle()
  228. await model_client.close()
  229. if __name__ == "__main__":
  230. parser = argparse.ArgumentParser(description="Run a chess game between two agents.")
  231. parser.add_argument("--verbose", action="store_true", help="Enable verbose logging.")
  232. parser.add_argument(
  233. "--model-config", type=str, help="Path to the model configuration file.", default="model_config.yml"
  234. )
  235. args = parser.parse_args()
  236. if args.verbose:
  237. logging.basicConfig(level=logging.WARNING)
  238. logging.getLogger("autogen_core").setLevel(logging.DEBUG)
  239. handler = logging.FileHandler("chess_game.log")
  240. logging.getLogger("autogen_core").addHandler(handler)
  241. with open(args.model_config, "r") as f:
  242. model_config = yaml.safe_load(f)
  243. asyncio.run(main(model_config))