diff --git a/.github/workflows/codex-pr-review.yml.disabled b/.github/workflows/codex-pr-review.yml.disabled new file mode 100644 index 0000000..66b3dd8 --- /dev/null +++ b/.github/workflows/codex-pr-review.yml.disabled @@ -0,0 +1,64 @@ +name: Codex PR Review +on: + pull_request: + types: [opened] + +jobs: + codex: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + final_message: ${{ steps.run_codex.outputs.final-message }} + steps: + - uses: actions/checkout@v5 + with: + # Explicitly check out the PR's merge commit. + ref: refs/pull/${{ github.event.pull_request.number }}/merge + + - name: Pre-fetch base and head refs for the PR + run: | + git fetch --no-tags origin \ + ${{ github.event.pull_request.base.ref }} \ + +refs/pull/${{ github.event.pull_request.number }}/head + + - name: Run Codex + id: run_codex + uses: openai/codex-action@v1 + with: + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + prompt: | + This is PR #${{ github.event.pull_request.number }} for ${{ github.repository }}. + + Review ONLY the changes introduced by the PR, so consider: + git log --oneline ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} + + Suggest any improvements, potential bugs, or issues. + Be concise and specific in your feedback. + + Pull request title and body: + ---- + ${{ github.event.pull_request.title }} + ${{ github.event.pull_request.body }} + + post_feedback: + runs-on: ubuntu-latest + needs: codex + if: needs.codex.outputs.final_message != '' + permissions: + issues: write + pull-requests: write + steps: + - name: Report Codex feedback + uses: actions/github-script@v7 + env: + CODEX_FINAL_MESSAGE: ${{ needs.codex.outputs.final_message }} + with: + github-token: ${{ github.token }} + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: process.env.CODEX_FINAL_MESSAGE, + }); diff --git a/.github/workflows/pr-tests-and-format.yml b/.github/workflows/pr-tests-and-format.yml new file mode 100644 index 0000000..0dfd168 --- /dev/null +++ b/.github/workflows/pr-tests-and-format.yml @@ -0,0 +1,26 @@ +name: PR Tests and Formatting + +on: + pull_request: + +jobs: + lint-and-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up uv + uses: astral-sh/setup-uv@v2 + with: + python-version: "3.12" + cache: true + + - name: Install dependencies + run: uv sync --frozen + + - name: Check formatting with Black + run: uv run black . --check + + - name: Run pytest + run: uv run pytest diff --git a/pyproject.toml b/pyproject.toml index 5798ca1..1a48187 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,8 @@ dependencies = [ "trustcall", "python-dotenv", "pytest", - "PyYAML" + "PyYAML", + "black" ] [tool.pytest.ini_options] diff --git a/src/game/__init__.py b/src/game/__init__.py index 53c3504..0fbadf2 100644 --- a/src/game/__init__.py +++ b/src/game/__init__.py @@ -4,9 +4,17 @@ from . import nodes from . import state -from . import llm_strategy +from .llm_strategy import llm_update_player_mindset, llm_generate_speech from . import rules from . import graph from . import config -__all__ = ["nodes", "state", "llm_strategy", "rules", "graph", "config"] +__all__ = [ + "nodes", + "state", + "llm_update_player_mindset", + "llm_generate_speech", + "rules", + "graph", + "config", +] diff --git a/src/game/llm_strategy.py b/src/game/llm_strategy.py index 7e47d82..b58bdc7 100644 --- a/src/game/llm_strategy.py +++ b/src/game/llm_strategy.py @@ -28,18 +28,14 @@ from typing import List, Dict, Any, Sequence from langchain_core.messages import HumanMessage, SystemMessage - from trustcall import create_extractor -from src.game.state import GameState, Speech, PlayerMindset, Suspicion, SelfBelief -from src.tools.llm import create_llm +from src.game.state import Speech, PlayerMindset, Suspicion, SelfBelief + # Game rules are now managed by the configuration system # Use config.get_game_rules() to get game rules -# --- LLM Clients --- -llm_client = create_llm() - # --- Helper Functions --- diff --git a/src/game/nodes/player.py b/src/game/nodes/player.py index 5c04062..c375fce 100644 --- a/src/game/nodes/player.py +++ b/src/game/nodes/player.py @@ -27,9 +27,9 @@ from ..llm_strategy import ( llm_update_player_mindset, llm_generate_speech, - llm_client, # Use the real LLM client ) from ..metrics import metrics_collector +from src.tools.llm import create_llm from ..state import ( GameState, alive_players, @@ -42,7 +42,14 @@ merge_probs, ) -_llm_client = llm_client + +def _get_llm_client(): + """Create and return an LLM client instance. + + This function provides lazy initialization of the LLM client, + creating it only when needed and allowing for runtime configuration. + """ + return create_llm() def _get_player_context(state: GameState, player_id: str): @@ -104,8 +111,9 @@ def player_speech(state: GameState, player_id: str) -> Dict[str, Any]: private_state = cur_player_context["private"] existing_player_mindset = private_state.playerMindset + llm_client = _get_llm_client() updated_mindset = llm_update_player_mindset( - llm_client=_llm_client, + llm_client=llm_client, my_word=my_word, completed_speeches=state["completed_speeches"], players=state["players"], @@ -117,7 +125,7 @@ def player_speech(state: GameState, player_id: str) -> Dict[str, Any]: # Generate speech using LLM new_speech_text = llm_generate_speech( - llm_client=_llm_client, + llm_client=llm_client, my_word=my_word, self_belief=updated_mindset.self_belief, suspicions=updated_mindset.suspicions, @@ -236,8 +244,9 @@ def player_vote(state: GameState, player_id: str) -> Dict[str, Any]: private_state = cur_player_context["private"] existing_player_mindset = private_state.playerMindset + llm_client = _get_llm_client() updated_mindset = llm_update_player_mindset( - llm_client=_llm_client, + llm_client=llm_client, my_word=my_word, completed_speeches=state["completed_speeches"], players=state["players"], diff --git a/tests/test_player_nodes.py b/tests/test_player_nodes.py index e116000..c13a760 100644 --- a/tests/test_player_nodes.py +++ b/tests/test_player_nodes.py @@ -70,13 +70,17 @@ def base_player_state(player_id): } +@patch("src.game.nodes.player._get_llm_client") @patch("src.game.nodes.player.llm_generate_speech") @patch("src.game.nodes.player.llm_update_player_mindset") def test_player_speech( - mock_infer, mock_speech, player_id, base_player_state: GameState + mock_infer, mock_speech, mock_get_llm, player_id, base_player_state: GameState ): """Tests the player_speech node with mocked LLM calls.""" # Arrange: Configure mocks to return predictable values + mock_llm_client = MagicMock() + mock_get_llm.return_value = mock_llm_client + mock_infer.return_value = PlayerMindset( self_belief=SelfBelief(role="civilian", confidence=0.9), suspicions={"c": Suspicion(role="spy", confidence=0.7, reason="vague")}, @@ -98,14 +102,19 @@ def test_player_speech( assert private_update.playerMindset.suspicions["c"].role == "spy" # Verify mocks were called correctly + mock_get_llm.assert_called_once() mock_infer.assert_called_once() mock_speech.assert_called_once() +@patch("src.game.nodes.player._get_llm_client") @patch("src.game.nodes.player.llm_update_player_mindset") -def test_player_vote(mock_infer, player_id, base_player_state: GameState): +def test_player_vote(mock_infer, mock_get_llm, player_id, base_player_state: GameState): """Tests the player_vote node with mocked LLM calls.""" # Arrange: Configure mocks + mock_llm_client = MagicMock() + mock_get_llm.return_value = mock_llm_client + mock_infer.return_value = PlayerMindset( self_belief=SelfBelief(role="civilian", confidence=0.9), suspicions={"c": Suspicion(role="spy", confidence=0.8, reason="very vague")}, @@ -129,6 +138,7 @@ def test_player_vote(mock_infer, player_id, base_player_state: GameState): assert private_update.playerMindset.self_belief.role == "civilian" # Verify mocks + mock_get_llm.assert_called_once() mock_infer.assert_called_once() diff --git a/uv.lock b/uv.lock index 41b49b1..4e1bc4e 100644 --- a/uv.lock +++ b/uv.lock @@ -145,6 +145,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "black" +version = "25.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/43/20b5c90612d7bdb2bdbcceeb53d588acca3bb8f0e4c5d5c751a2c8fdd55a/black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619", size = 648393, upload-time = "2025-09-19T00:27:37.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/8e/319cfe6c82f7e2d5bfb4d3353c6cc85b523d677ff59edc61fdb9ee275234/black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0", size = 1742012, upload-time = "2025-09-19T00:33:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/94/cc/f562fe5d0a40cd2a4e6ae3f685e4c36e365b1f7e494af99c26ff7f28117f/black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4", size = 1581421, upload-time = "2025-09-19T00:35:25.937Z" }, + { url = "https://files.pythonhosted.org/packages/84/67/6db6dff1ebc8965fd7661498aea0da5d7301074b85bba8606a28f47ede4d/black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e", size = 1655619, upload-time = "2025-09-19T00:30:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/10/10/3faef9aa2a730306cf469d76f7f155a8cc1f66e74781298df0ba31f8b4c8/black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a", size = 1342481, upload-time = "2025-09-19T00:31:29.625Z" }, + { url = "https://files.pythonhosted.org/packages/48/99/3acfea65f5e79f45472c45f87ec13037b506522719cd9d4ac86484ff51ac/black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175", size = 1742165, upload-time = "2025-09-19T00:34:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/3a/18/799285282c8236a79f25d590f0222dbd6850e14b060dfaa3e720241fd772/black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f", size = 1581259, upload-time = "2025-09-19T00:32:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ce/883ec4b6303acdeca93ee06b7622f1fa383c6b3765294824165d49b1a86b/black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831", size = 1655583, upload-time = "2025-09-19T00:30:44.505Z" }, + { url = "https://files.pythonhosted.org/packages/21/17/5c253aa80a0639ccc427a5c7144534b661505ae2b5a10b77ebe13fa25334/black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357", size = 1343428, upload-time = "2025-09-19T00:32:13.839Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/863c90dcd3f9d41b109b7f19032ae0db021f0b2a81482ba0a1e28c84de86/black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae", size = 203363, upload-time = "2025-09-19T00:27:35.724Z" }, +] + [[package]] name = "blockbuster" version = "1.5.25" @@ -1061,6 +1086,7 @@ name = "liegraph" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "black" }, { name = "httpx", extra = ["socks"] }, { name = "langchain-community" }, { name = "langchain-core" }, @@ -1076,6 +1102,7 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "black" }, { name = "httpx", extras = ["socks"] }, { name = "langchain-community" }, { name = "langchain-core" }, @@ -1469,6 +1496,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -1725,6 +1770,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, ] +[[package]] +name = "pytokens" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/c2/dbadcdddb412a267585459142bfd7cc241e6276db69339353ae6e241ab2b/pytokens-0.2.0.tar.gz", hash = "sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43", size = 15368, upload-time = "2025-10-15T08:02:42.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/5a/c269ea6b348b6f2c32686635df89f32dbe05df1088dd4579302a6f8f99af/pytokens-0.2.0-py3-none-any.whl", hash = "sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8", size = 12038, upload-time = "2025-10-15T08:02:41.694Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3"