From 62ebc4eed802999f3b5bba7570df99155c1268c5 Mon Sep 17 00:00:00 2001 From: ehsanmok Date: Wed, 26 Feb 2025 06:13:36 +0000 Subject: [PATCH] Init agentic puzzles --- agentic-puzzles/.gitattributes | 2 + agentic-puzzles/.gitignore | 6 + agentic-puzzles/multiagent_chess.py | 373 ++++++++++++++++++++++++++++ agentic-puzzles/pyproject.toml | 24 ++ agentic-puzzles/utils.py | 37 +++ 5 files changed, 442 insertions(+) create mode 100644 agentic-puzzles/.gitattributes create mode 100644 agentic-puzzles/.gitignore create mode 100644 agentic-puzzles/multiagent_chess.py create mode 100644 agentic-puzzles/pyproject.toml create mode 100644 agentic-puzzles/utils.py diff --git a/agentic-puzzles/.gitattributes b/agentic-puzzles/.gitattributes new file mode 100644 index 0000000..887a2c1 --- /dev/null +++ b/agentic-puzzles/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true diff --git a/agentic-puzzles/.gitignore b/agentic-puzzles/.gitignore new file mode 100644 index 0000000..9279c11 --- /dev/null +++ b/agentic-puzzles/.gitignore @@ -0,0 +1,6 @@ + +# pixi environments +.pixi +*.egg-info +# magic environments +.magic diff --git a/agentic-puzzles/multiagent_chess.py b/agentic-puzzles/multiagent_chess.py new file mode 100644 index 0000000..12faed3 --- /dev/null +++ b/agentic-puzzles/multiagent_chess.py @@ -0,0 +1,373 @@ +import os +import asyncio +import chess +from autogen_agentchat.agents import AssistantAgent, SocietyOfMindAgent +from autogen_agentchat.teams import RoundRobinGroupChat +from autogen_agentchat.conditions import TextMentionTermination +from autogen_agentchat.agents import AssistantAgent +from autogen_ext.models.openai import OpenAIChatCompletionClient +from autogen_agentchat.teams import RoundRobinGroupChat +from rich.console import Console +from rich.panel import Panel +from rich.prompt import Prompt + +from utils import patch_openai_client_usage_tracking + +patch_openai_client_usage_tracking() + + +os.environ["RICH_TRACEBACK"] = "1" # 0=silent, 1=minimal, 2=full +LLM_MODEL = os.getenv("LLM_MODEL", "Qwen/Qwen2.5-7B-Instruct-1M") +LLM_FAMILY = os.getenv("LLM_FAMILY", "qwen") +LLM_SERVER_URL = os.getenv("LLM_SERVER_URL", "http://localhost:8010/v1") +LLM_API_KEY = os.getenv("LLM_API_KEY", "local") + + +def create_base_client(base_url: str = LLM_SERVER_URL, temperature: float = 0.0): + """Create base OpenAI client with DeepSeek model.""" + return OpenAIChatCompletionClient( + model=LLM_MODEL, + base_url=LLM_SERVER_URL, + api_key=LLM_API_KEY, + model_info={ + "name": LLM_MODEL, + "family": LLM_FAMILY, + "pricing": {"prompt": 0.0, "completion": 0.0}, + "function_calling": False, + "vision": False, + "json_output": False, + }, + temperature=temperature, + max_tokens=512, + track_usage=False, + ) + +def create_chess_agents(board: chess.Board, feedback: str = ""): + """Create the team of chess agents using SocietyOfMindAgent.""" + client = create_base_client(temperature=0.1) + + last_move = board.peek().uci() if board.move_stack else 'None' + + threatened_pieces = [] + for square, piece in board.piece_map().items(): + if piece.color == chess.BLACK and board.is_attacked_by(chess.WHITE, square): + threatened_pieces.append(f"{piece.symbol().upper()} on {chess.square_name(square)}") + + position_info = f"""CURRENT POSITION: +{board.unicode()} + +CHESS RULES: +1. Pawns move forward one square (or two on first move) +2. Pawns capture diagonally only when enemy piece present +3. Knights move in L-shape (2+1) +4. Bishops move diagonally +5. Rooks move horizontally/vertically +6. Queen combines bishop + rook moves +7. King moves one square any direction + +PIECE VALUES: +- Pawn = 1 point +- Knight = 3 points +- Bishop = 3 points +- Rook = 5 points +- Queen = 9 points +- King = ∞ (must be protected) + +IMPORTANT: +- After White's move {last_move}, check if any of your pieces are under attack! +- Threatened Black pieces: {', '.join(threatened_pieces) if threatened_pieces else 'None'} +- Don't lose pieces for free - move threatened pieces to safety! + +BLACK pieces: {', '.join(f"{piece.symbol().upper()} on {chess.square_name(square)}" + for square, piece in board.piece_map().items() + if piece.color == chess.BLACK)} +WHITE pieces: {', '.join(f"{piece.symbol().upper()} on {chess.square_name(square)}" + for square, piece in board.piece_map().items() + if piece.color == chess.WHITE)} +LEGAL MOVES: {', '.join(move.uci() for move in board.legal_moves)}""" + + if feedback: + position_info = f"{position_info}\n\nPREVIOUS ERROR: {feedback}" + + evaluator = AssistantAgent( + name="chess_evaluator", + system_message=f"""{position_info} + + +YOU ARE A CHESS GRANDMASTER PLAYING BLACK. White's last move was: {last_move} +1. FIRST CHECK: Are any of your pieces under attack? Save them! +2. Then check if you can capture any White pieces +3. Choose ONE move from LEGAL MOVES list +Output using EXACTLY this format: +e4f6 +""", + model_client=client, + ) + + generator = AssistantAgent( + name="chess_generator", + system_message=f"""{position_info} + + +YOU ARE A CHESS GRANDMASTER PLAYING BLACK. White's last move was: {last_move} +1. FIRST CHECK: Are any of your pieces under attack? Suggest moves to save them! +2. Then suggest captures if available +3. Choose THREE moves from LEGAL MOVES list +Output using EXACTLY this format: + +move1: e4f6 | Saves knight from attack +move2: e4g5 | Moves knight to safety +move3: e4c5 | Retreats to safe square +""", + model_client=client, + ) + + selector = AssistantAgent( + name="chess_selector", + system_message=f"""{position_info} + + +YOU ARE A CHESS GRANDMASTER PLAYING BLACK. White's last move was: {last_move} +1. FIRST CHECK: Are any pieces under attack? Choose a move that saves them! +2. Then check for good captures +3. Choose ONE move that is in LEGAL MOVES list +4. You MUST output EXACTLY these two lines with no other text: +g8f6 +APPROVE +""", + model_client=client, + ) + + inner_termination = TextMentionTermination("APPROVE") + inner_team = RoundRobinGroupChat( + [evaluator, generator, selector], + termination_condition=inner_termination, + max_turns=3 + ) + + chess_team = SocietyOfMindAgent( + name="chess_team", + team=inner_team, + model_client=client, + description="Extract final move", + instruction="Wait for tag and APPROVE", + response_prompt="Extract move from tag" + ) + + return chess_team + +async def get_team_move(board: chess.Board, team, console: Console) -> str: + """Get a move from the chess team using SocietyOfMindAgent.""" + legal_moves = [move.uci() for move in board.legal_moves] + + position_info = f"""CURRENT POSITION (Black to move): +{board.unicode()} + +BLACK pieces: {', '.join(f"{piece.symbol().upper()} on {chess.square_name(square)}" + for square, piece in board.piece_map().items() + if piece.color == chess.BLACK)} +WHITE pieces: {', '.join(f"{piece.symbol().upper()} on {chess.square_name(square)}" + for square, piece in board.piece_map().items() + if piece.color == chess.WHITE)} +WHITE's last move: {board.peek().uci() if board.move_stack else 'None'} +LEGAL MOVES: {', '.join(legal_moves)}""" + + console.print("\n[bold yellow]DEBUG: Starting position info:[/bold yellow]") + console.print(Panel(position_info)) + + feedback = "" + tried_moves = set() + + for attempt in range(3): + try: + chess_team = create_chess_agents(board, feedback) + + with console.status(f"[bold green]AI is analyzing the position (attempt {attempt + 1}/3)...[/bold green]"): + stream = chess_team.run_stream(task=position_info) + async for message in stream: + if "TaskResult" in str(message): + console.print("[dim]Skipping TaskResult message[/dim]") + continue + + if hasattr(message, 'content') and hasattr(message, 'source'): + content = message.content + source = message.source + + if source == "user": + console.print("[dim]Skipping user message[/dim]") + continue + + console.print(Panel( + content, + title=f"[bold]Agent: {source}[/bold]", + border_style="cyan" + )) + + try: + if "" in content and "APPROVE" in content: + try: + move = extract_move(content) + move = move.replace('-', '') + + if move in tried_moves: + feedback = f"Already tried move {move}. Please choose a different move from: {', '.join(legal_moves)}" + raise ValueError(f"Already tried: {move}") + + tried_moves.add(move) + + if move in legal_moves: + console.print(Panel( + f"[bold green]Black plays: {move}[/bold green]", + title="[bold green]♟️ Move Selected[/bold green]", + border_style="green" + )) + return move + else: + feedback = f"Move {move} is not legal. Legal moves are: {', '.join(legal_moves)}" + raise ValueError(f"Illegal move: {move}") + except ValueError as e: + console.print(f"[bold red]Move extraction failed: {str(e)}[/bold red]") + raise + + except Exception as e: + console.print(f"[bold red]Error processing message: {str(e)}[/bold red]") + raise + + if attempt < 2: + console.print(f"\n[yellow]Attempt {attempt + 1} failed. Creating new team and retrying...[/yellow]") + + except Exception as e: + if attempt == 2: + raise ValueError(f"Failed to get legal move after 3 attempts: {str(e)}") + console.print(f"\n[red]Error in attempt {attempt + 1}: {str(e)}[/red]") + console.print(f"[yellow]Retrying with feedback: {feedback}[/yellow]") + continue + + raise ValueError("Failed to get legal move - no valid move found in responses") + +async def main() -> None: + console = Console() + + try: + board = chess.Board() + assert board.fen().split()[0] == "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" + + console.print(Panel.fit("♟️ Welcome to Chess vs AI!\n\nYou play as White, AI plays as Black\nUse UCI format for moves (e.g., 'e2e4')\n(Type 'exit' to leave)", + border_style="blue", + title="Chess Game")) + + while not board.is_game_over(): + console.print(Panel( + format_board(board), + title=f"[bold]Move {board.fullmove_number}[/bold]", + border_style="blue" + )) + + if board.turn == chess.WHITE: + while True: + move_str = Prompt.ask("\n[bold blue]Your move[/bold blue]") + if move_str.lower() == 'exit': + console.print("\n[yellow]Game ended by player. Thanks for playing![/yellow]") + return + + try: + move = chess.Move.from_uci(move_str) + if move in board.legal_moves: + board.push(move) + # Show board after White's move + console.print("\n[bold white]Position after your move:[/bold white]") + console.print(Panel( + format_board(board), + title=f"[bold]After {move_str}[/bold]", + border_style="white" + )) + break + console.print("[red]Illegal move! Try again.[/red]") + console.print(f"[yellow]Legal moves are: {', '.join(move.uci() for move in board.legal_moves)}[/yellow]") + except ValueError: + console.print("[red]Invalid format! Use UCI notation (e.g., 'e2e4')[/red]") + + else: + console.print("\n[bold green]AI is thinking...[/bold green]") + try: + chess_team = create_chess_agents(board) + move = await get_team_move(board, chess_team, console) + board.push(chess.Move.from_uci(move)) + except ValueError as e: + console.print(f"[red]Error: {str(e)}[/red]") + return + + console.print(Panel(format_board(board), title="Final Position")) + console.print(f"\n[bold]Game Over! Result: {board.result()}[/bold]") + if board.is_checkmate(): + console.print("[bold]Checkmate![/bold]") + elif board.is_stalemate(): + console.print("[bold]Stalemate![/bold]") + elif board.is_insufficient_material(): + console.print("[bold]Draw by insufficient material![/bold]") + elif board.is_fifty_moves(): + console.print("[bold]Draw by fifty-move rule![/bold]") + elif board.is_repetition(): + console.print("[bold]Draw by repetition![/bold]") + except KeyboardInterrupt: + console.print("\n[yellow]Game interrupted. Thanks for playing![/yellow]") + except Exception as e: + console.print(f"\n[red]An error occurred: {str(e)}[/red]") + +def format_board(board: chess.Board) -> str: + pieces = { + 'r': '♜', 'n': '♞', 'b': '♝', 'q': '♛', 'k': '♚', 'p': '♟', + 'R': '♖', 'N': '♘', 'B': '♗', 'Q': '♕', 'K': '♔', 'P': '♙', + '.': ' ' + } + + rows = [] + board_state = str(board).split('\n') + + for rank in range(8, 0, -1): + squares = [f"[dim] {rank} [/dim]"] + row = board_state[8-rank] + + for file in range(8): + is_light_square = (rank + file) % 2 == 0 + bg_color = "rgb(240,217,181)" if is_light_square else "rgb(181,136,99)" + piece = row.split()[file] + piece_symbol = pieces.get(piece, pieces['.']) + + if piece.isupper(): + text_style = f"white on {bg_color}" + elif piece.islower(): + text_style = f"black on {bg_color}" + else: + text_style = f"on {bg_color}" + + squares.append(f"[{text_style}] {piece_symbol} [/]") + + rows.append("".join(squares)) + + files = " " + "".join(f"[dim] {chr(97+i)} [/dim]" for i in range(8)) + rows.append(files) + + return "\n".join(rows) + +def extract_move(content: str) -> str: + """Extract move from content with better error handling.""" + try: + if '' in content and '' in content: + move_start = content.index('') + 6 + move_end = content.index('') + move = content[move_start:move_end].strip() + return move.replace('-', '') + + words = content.split() + for word in words: + if len(word) == 4 and word[0].isalpha() and word[1].isdigit() and \ + word[2].isalpha() and word[3].isdigit(): + return word + + raise ValueError("No valid move format found") + except Exception as e: + raise ValueError(f"Move extraction failed: {str(e)}") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/agentic-puzzles/pyproject.toml b/agentic-puzzles/pyproject.toml new file mode 100644 index 0000000..81185d1 --- /dev/null +++ b/agentic-puzzles/pyproject.toml @@ -0,0 +1,24 @@ +[project] +authors = [{ name = "Modular Inc", email = "hello@modular.com" }] +dependencies = ["autogen-agentchat==0.4.7", "autogen-ext[openai]==0.4.7", "rich>=13.9.4,<14", "python-chess>=1.999,<2", "pydantic>=2.10.6,<3"] +name = "agentic-puzzles" +requires-python = ">=3.10,<3.13" +version = "0.0.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.pixi.project] +channels = ["https://conda.modular.com/max-nightly", "https://conda.modular.com/max", "https://repo.prefix.dev/modular-community", "conda-forge"] +platforms = ["linux-64"] + +[tool.pixi.pypi-dependencies] +agentic_puzzles = { path = ".", editable = true } + +[tool.pixi.tasks] +server = "(magic global install max-pipelines && magic global update max-pipelines) || true; MAX_SERVE_PORT=8010 MAX_SERVE_HOST=0.0.0.0 max-pipelines serve --huggingface-repo-id=Qwen/Qwen2.5-7B-Instruct-1M --max-length 128000 --max-batch-size 1" +chess = "magic run python multiagent_chess.py" diff --git a/agentic-puzzles/utils.py b/agentic-puzzles/utils.py new file mode 100644 index 0000000..c51b5ff --- /dev/null +++ b/agentic-puzzles/utils.py @@ -0,0 +1,37 @@ + +def patch_openai_client_usage_tracking(): + import autogen_ext.models.openai._openai_client as openai_client + + original_add_usage = openai_client._add_usage + + def patched_add_usage(usage1, usage2): + if usage1 is None: + return usage2 + if usage2 is None: + return usage1 + + prompt_tokens1 = getattr(usage1, 'prompt_tokens', 0) or 0 + completion_tokens1 = getattr(usage1, 'completion_tokens', 0) or 0 + total_tokens1 = getattr(usage1, 'total_tokens', 0) or 0 + + prompt_tokens2 = getattr(usage2, 'prompt_tokens', 0) or 0 + completion_tokens2 = getattr(usage2, 'completion_tokens', 0) or 0 + total_tokens2 = getattr(usage2, 'total_tokens', 0) or 0 + + from dataclasses import dataclass + + @dataclass + class Usage: + prompt_tokens: int + completion_tokens: int + total_tokens: int + + return Usage( + prompt_tokens=prompt_tokens1 + prompt_tokens2, + completion_tokens=completion_tokens1 + completion_tokens2, + total_tokens=total_tokens1 + total_tokens2 + ) + + openai_client._add_usage = patched_add_usage + + return original_add_usage