diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4244703ae..42f34cc1c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,7 +21,8 @@ "ghcr.io/devcontainers/features/python:1": { "installTools": true, "version": "latest" - } + }, + "ghcr.io/devcontainers-contrib/features/hatch:2": {} }, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 5f781d060..5383fe846 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -9,68 +9,53 @@ on: - main jobs: - lint: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - run: pip install ".[dev]" - - run: ruff check - format: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - run: pip install ".[dev]" - - run: ruff format --check + - name: Install Hatch + uses: pypa/hatch@install + - run: hatch run ruff format --check + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Hatch + uses: pypa/hatch@install + - run: hatch run ruff check mypy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - run: pip install ".[dev]" - - run: mypy + - name: Install Hatch + uses: pypa/hatch@install + - run: hatch run mypy pyright: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - run: pip install ".[dev]" - - uses: jakebailey/pyright-action@v2 + - name: Install Hatch + uses: pypa/hatch@install + - run: hatch run pyright test: runs-on: ubuntu-latest strategy: matrix: - # "pypy3.10" disabled until better example than polars used in tests python-version: ["3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - run: pip install ".[dev]" - - run: pytest -n auto + - name: Install Hatch + uses: pypa/hatch@install + - run: hatch run +python=${{ matrix.python-version }} test-matrix:pytest -n auto docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - run: pip install ".[docs]" - - run: sphinx-build --fail-on-warning docs/src docs/build + - name: Install Hatch + uses: pypa/hatch@install + - run: hatch run docs:check diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5b64cef16..333b144c0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -30,11 +30,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - run: pip install ".[docs]" - - run: sphinx-build docs/src docs/build + - name: Install Hatch + uses: pypa/hatch@install + - run: hatch run docs:build - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/README.md b/README.md index bb858fe5c..b818e28fe 100644 --- a/README.md +++ b/README.md @@ -14,50 +14,32 @@ ## Development +**TL;DR**, run all checks with: + +```sh +hatch run check +``` + ### Setup -```sh -python -m venv .venv -source .venv/bin/activate +- [Install `hatch`](https://hatch.pypa.io/1.12/install/). -pip install -e ".[dev]" +### Virtual environment + +To get a shell with the package available (virtual environment) run: +```sh +hatch shell ``` -### Running tests +### Common tasks -```sh -pytest -``` +- Format: `hatch run check` +- Lint: `hatch run lint` +- Test: `hatch run pytest -n auto` +- Mypy: `hatch run mypy` +- Pyright: `hatch run pyright` +- Build docs: `hatch run docs:build` +- Auto rebuild+serve docs: `hatch run docs:serve` -### Type checking - -```sh -mypy -``` - -```sh -pyright -``` - -### Linting - -```sh -ruff check -``` - -### Formatting - -```sh -ruff format -``` - -### Build docs - -```sh -pip install -e ".[docs]" - -sphinx-build docs/src docs/build - -# To view the docs: -python -m http.server -d docs/build -``` +> [!NOTE] +> These don't need to be run in a virtual environment, `hatch` will automatically manage it for you. diff --git a/examples/README.md b/examples/README.md index f218fa356..83e8640ce 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,23 +2,28 @@ This directory contains examples of how to use AGNext. -First, you need to install AGNext and development dependencies by running the -following command: +First, you need a shell with AGNext and the examples dependencies installed. To do this, run: ```bash -pip install -e '.[dev]' +hatch shell ``` To run an example, just run the corresponding Python script. For example, to run the `coder_reviewer.py` example, run: ```bash +hatch shell python coder_reviewer.py ``` +Or simply: +```bash +hatch run python coder_reviewer.py +``` + To enable logging, turn on verbose mode by setting `--verbose` flag: ```bash -python coder_reviewer.py --verbose +hatch run python coder_reviewer.py --verbose ``` By default the log file is saved in the same directory with the same filename diff --git a/pyproject.toml b/pyproject.toml index 8abfc9d30..752408f3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" +requires = ["hatchling"] +build-backend = "hatchling.build" [project] name = "agnext" @@ -21,29 +21,54 @@ dependencies = [ "pydantic>=1.10,<3", ] -[project.optional-dependencies] -dev = [ - "ruff==0.4.6", - "pyright", - "mypy", - "pytest", - "pytest-asyncio", - "pytest-xdist", +[tool.hatch.envs.default] +dependencies = [ + "pyright==1.1.368", + "mypy==1.10.0", + "ruff==0.4.8", "types-Pillow", "polars", - # Dependencies for the examples. "chess", "tavily-python", "aiofiles", "types-aiofiles", "colorama", "textual", + "textual-dev", "textual-imageview", + "pytest-asyncio", + "pip", + "pytest", + "pytest-xdist", ] -docs = ["sphinx", "furo", "sphinxcontrib-apidoc", "myst-parser"] -[tool.setuptools.package-data] -agnext = ["py.typed"] +[tool.hatch.envs.default.scripts] +fmt = "ruff format" +lint = "ruff check" +test = "pytest -n auto" +check = [ + "ruff format", + "ruff check --fix", + "pyright", + "mypy", + "pytest -n auto", +] + +[tool.hatch.envs.test-matrix] +template = "default" + +[[tool.hatch.envs.test-matrix.matrix]] +python = ["3.10", "3.11", "3.12"] + +[tool.hatch.envs.docs] +dependencies = [ + "sphinx", "furo", "sphinxcontrib-apidoc", "myst-parser", "sphinx-autobuild" +] + +[tool.hatch.envs.docs.scripts] +build = "sphinx-build docs/src docs/build" +serve = "sphinx-autobuild --watch src docs/src docs/build" +check = "sphinx-build --fail-on-warning docs/src docs/build" [tool.ruff] line-length = 120 diff --git a/src/agnext/application/_single_threaded_agent_runtime.py b/src/agnext/application/_single_threaded_agent_runtime.py index aa4d3456e..6a59d21c2 100644 --- a/src/agnext/application/_single_threaded_agent_runtime.py +++ b/src/agnext/application/_single_threaded_agent_runtime.py @@ -121,9 +121,6 @@ class SingleThreadedAgentRuntime(AgentRuntime): # ) # ) - if recipient.namespace not in self._known_namespaces: - self._prepare_namespace(recipient.namespace) - future = asyncio.get_event_loop().create_future() if recipient.name not in self._known_agent_names: future.set_exception(Exception("Recipient not found")) @@ -131,6 +128,8 @@ class SingleThreadedAgentRuntime(AgentRuntime): if sender is not None and sender.namespace != recipient.namespace: raise ValueError("Sender and recipient must be in the same namespace to communicate.") + self._process_seen_namespace(recipient.namespace) + logger.info(f"Sending message of type {type(message).__name__} to {recipient.name}: {message.__dict__}") self._message_queue.append( @@ -180,8 +179,7 @@ class SingleThreadedAgentRuntime(AgentRuntime): assert explicit_namespace is not None or sender_namespace is not None namespace = cast(str, explicit_namespace or sender_namespace) - if namespace not in self._known_namespaces: - self._prepare_namespace(namespace) + self._process_seen_namespace(namespace) self._message_queue.append( PublishMessageEnvelope( @@ -389,6 +387,11 @@ class SingleThreadedAgentRuntime(AgentRuntime): else: self._valid_namespaces[name] = [] + # For all already prepared namespaces we need to prepare this agent + for namespace in self._known_namespaces: + if self._type_valid_for_namespace(AgentId(name=name, namespace=namespace)): + self._get_agent(AgentId(name=name, namespace=namespace)) + def _invoke_agent_factory( self, agent_factory: Callable[[], T] | Callable[[AgentRuntime, AgentId], T], agent_id: AgentId ) -> T: @@ -419,13 +422,13 @@ class SingleThreadedAgentRuntime(AgentRuntime): return agent_id.namespace in valid_namespaces def _get_agent(self, agent_id: AgentId) -> Agent: + self._process_seen_namespace(agent_id.namespace) if agent_id in self._instantiated_agents: return self._instantiated_agents[agent_id] if not self._type_valid_for_namespace(agent_id): raise ValueError(f"Agent with name {agent_id.name} not valid for namespace {agent_id.namespace}.") - self._known_namespaces.add(agent_id.namespace) if agent_id.name not in self._agent_factories: raise ValueError(f"Agent with name {agent_id.name} not found.") @@ -446,7 +449,11 @@ class SingleThreadedAgentRuntime(AgentRuntime): # Hydrate the agent instances in a namespace. The primary reason for this is # to ensure message type subscriptions are set up. - def _prepare_namespace(self, namespace: str) -> None: + def _process_seen_namespace(self, namespace: str) -> None: + if namespace in self._known_namespaces: + return + + self._known_namespaces.add(namespace) for name in self._known_agent_names: if self._type_valid_for_namespace(AgentId(name=name, namespace=namespace)): self._get_agent(AgentId(name=name, namespace=namespace)) diff --git a/src/agnext/components/_function_utils.py b/src/agnext/components/_function_utils.py index 4628ae57a..d541411d4 100644 --- a/src/agnext/components/_function_utils.py +++ b/src/agnext/components/_function_utils.py @@ -248,7 +248,11 @@ def get_function_schema(f: Callable[..., Any], *, name: Optional[str] = None, de .. code-block:: python - def f(a: Annotated[str, "Parameter a"], b: int = 2, c: Annotated[float, "Parameter c"] = 0.1) -> None: + def f( + a: Annotated[str, "Parameter a"], + b: int = 2, + c: Annotated[float, "Parameter c"] = 0.1, + ) -> None: pass diff --git a/src/agnext/py.typed b/src/agnext/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/test.sh b/test.sh deleted file mode 100755 index 9236c93ff..000000000 --- a/test.sh +++ /dev/null @@ -1,23 +0,0 @@ -set -e - -# Current script directory -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -if [[ "$VIRTUAL_ENV" == "" ]] -then - echo "Virtual environment is not activated" - exit 1 -fi - -cd $DIR - -echo "--- Running ruff format ---" -ruff format -echo "--- Running ruff check ---" -ruff check -echo "--- Running pyright ---" -pyright -echo "--- Running mypy ---" -mypy -echo "--- Running pytest ---" -pytest -n auto \ No newline at end of file