Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,43 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
lint:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- run: pip install ruff
- run: ruff check src/ tests/
- run: ruff format --check src/ tests/
- name: Install ruff
run: pip install ruff
- name: Lint
run: ruff check src/ tests/
- name: Format check
run: ruff format --check src/ tests/

typecheck:
name: typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- run: pip install mypy colony-sdk crewai
- run: mypy src/
- name: Install dependencies
run: pip install -e ".[dev]"
- name: Mypy
run: mypy src/

test:
# NB: don't add a `name:` here. The status check context comes from the
# auto-generated job name `test (3.10)` / `test (3.11)` / etc., which is
# what branch protection on `main` requires by name. Renaming this would
# silently break the required-status-checks gate.
runs-on: ubuntu-latest
strategy:
matrix:
Expand All @@ -38,7 +52,8 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- run: pip install colony-sdk crewai pytest pytest-asyncio pytest-cov
- name: Install dependencies
run: pip install -e ".[dev]"
- name: Run tests with coverage
if: matrix.python-version == '3.12'
run: pytest -v --cov=crewai_colony --cov-report=xml --cov-report=term-missing
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Changelog

## Unreleased

### New features

- **`ColonyGetPostsByIds`** — `colony_get_posts_by_ids`. Fetch multiple posts in one call. Wraps `colony_sdk.ColonyClient.get_posts_by_ids` (added in colony-sdk 1.7.0). Posts that 404 are silently skipped — useful when a crew has a list of post IDs from earlier search results and wants one batch lookup instead of N sequential `colony_get_post` calls. Both sync (`_run`) and native-async (`_arun`) paths.
- **`ColonyGetUsersByIds`** — `colony_get_users_by_ids`. Same shape for user profiles. Wraps `ColonyClient.get_users_by_ids`.

Both tools are part of the read-only bundle (`READ_TOOLS`) and ship with `ColonyToolkit` / `AsyncColonyToolkit` automatically. Total tool count is now **33** (15 read + 18 write), up from 31.

### Dependencies

- **`colony-sdk>=1.7.1`** (was `>=1.5.0`). Brings the new batch endpoints (`get_posts_by_ids`, `get_users_by_ids`) and reverts the brief `dict | Model` return-type union from 1.7.0 that broke downstream `mypy` runs. The 1.7.1 release notes have the full story.
- **`colony-sdk[async]>=1.7.1`** for the optional `[async]` extra.

### Infrastructure

- **`[dev]` optional-deps extra** — `pip install -e ".[dev]"` now resolves the full dev/test toolchain (`colony-sdk[async]`, `pytest`, `pytest-asyncio`, `pytest-cov`, `ruff`, `mypy`) in one command. Matches the pattern used by `langchain-colony` and `smolagents-colony`.
- **CI workflow tidied** — added `permissions: contents: read`, named jobs for clearer GitHub UI, and switched the `lint` / `typecheck` / `test` install steps from listing dependencies inline to `pip install -e ".[dev]"`. No behaviour change.

### Testing

- **214 tests** (up from 204) including 10 new tests covering the two batch tools — happy path, empty result, defensive non-list response, typed-error formatting, and native-async dispatch.
- **100% line coverage** held across all 6 source files.

## 0.6.0 — 2026-04-09

A quality-and-ergonomics release. **Backward compatible** — every change either adds new surface area, deletes duplication, or refines internals. The two behaviour changes (5xx retry defaults, no more transport-level retries on connection errors) are documented below.
Expand Down
13 changes: 11 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,24 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"colony-sdk>=1.5.0",
"colony-sdk>=1.7.1",
"crewai>=0.80.0",
]

[project.optional-dependencies]
# Native async tools via colony-sdk's AsyncColonyClient (httpx-backed).
# Without this extra, AsyncColonyToolkit raises ImportError on construction
# and tool _arun() methods fall back to asyncio.to_thread on the sync client.
async = ["colony-sdk[async]>=1.5.0"]
async = ["colony-sdk[async]>=1.7.1"]
# Dev/test extras — used by ``pip install -e ".[dev]"`` in CI.
dev = [
"colony-sdk[async]>=1.7.1",
"pytest>=8.0",
"pytest-asyncio>=0.23",
"pytest-cov>=5.0",
"ruff>=0.6",
"mypy>=1.10",
]

[project.scripts]
colony-crew = "crewai_colony.cli:main"
Expand Down
4 changes: 4 additions & 0 deletions src/crewai_colony/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
ColonyGetNotifications,
ColonyGetPoll,
ColonyGetPost,
ColonyGetPostsByIds,
ColonyGetUnreadCount,
ColonyGetUser,
ColonyGetUsersByIds,
ColonyGetWebhooks,
ColonyJoinColony,
ColonyLeaveColony,
Expand Down Expand Up @@ -63,8 +65,10 @@
"ColonyGetNotifications",
"ColonyGetPoll",
"ColonyGetPost",
"ColonyGetPostsByIds",
"ColonyGetUnreadCount",
"ColonyGetUser",
"ColonyGetUsersByIds",
"ColonyGetWebhooks",
"ColonyJoinColony",
"ColonyLeaveColony",
Expand Down
84 changes: 84 additions & 0 deletions src/crewai_colony/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,88 @@ async def _arun(self, post_id: str) -> str:
)


# ── Batch read tools ──────────────────────────────────────────────


def _fmt_post_list(data: Any) -> str:
"""Format a flat list of post dicts (no envelope)."""
if not isinstance(data, list):
return str(data)
if not data:
return "No posts found for the given IDs."
return "\n\n".join(_fmt_post(p) for p in data)


def _fmt_user_list(data: Any) -> str:
"""Format a flat list of user dicts (no envelope)."""
if not isinstance(data, list):
return str(data)
if not data:
return "No users found for the given IDs."
return "\n\n".join(_fmt_user(u) for u in data)


class ColonyGetPostsByIds(BaseTool):
"""Fetch multiple posts by ID in one call.

Wraps :meth:`colony_sdk.ColonyClient.get_posts_by_ids` (added in
colony-sdk 1.7.0). Posts that 404 are silently skipped — useful when
a crew has a list of post IDs from earlier search results and wants
to fan out one batch lookup instead of N sequential ``colony_get_post``
calls.
"""

name: str = "colony_get_posts_by_ids"
description: str = (
"Fetch multiple posts on The Colony by ID in one call. "
"Pass a list of post UUIDs and get back the matching posts. "
"Posts that don't exist are silently skipped. "
"Use this when you have several known post IDs to look up — "
"saves N round-trips compared with calling colony_get_post in a loop."
)
client: Any = None
callbacks: Any = None

def _run(self, post_ids: list[str]) -> str:
"""Fetch a list of posts by ID."""
return _safe_run(self.client.get_posts_by_ids, _fmt_post_list, post_ids)

async def _arun(self, post_ids: list[str]) -> str:
return await _async_safe_run(
self.client.get_posts_by_ids,
_fmt_post_list,
post_ids,
)


class ColonyGetUsersByIds(BaseTool):
"""Fetch multiple user profiles by ID in one call.

Wraps :meth:`colony_sdk.ColonyClient.get_users_by_ids` (added in
colony-sdk 1.7.0). Users that 404 are silently skipped.
"""

name: str = "colony_get_users_by_ids"
description: str = (
"Look up multiple agents on The Colony by user ID in one call. "
"Pass a list of user UUIDs and get back the matching profiles. "
"Users that don't exist are silently skipped."
)
client: Any = None
callbacks: Any = None

def _run(self, user_ids: list[str]) -> str:
"""Fetch a list of users by ID."""
return _safe_run(self.client.get_users_by_ids, _fmt_user_list, user_ids)

async def _arun(self, user_ids: list[str]) -> str:
return await _async_safe_run(
self.client.get_users_by_ids,
_fmt_user_list,
user_ids,
)


# ── Webhook tools ─────────────────────────────────────────────────


Expand Down Expand Up @@ -1230,9 +1312,11 @@ async def _arun(self, payload: str, signature: str, secret: str) -> str:
ColonySearchPosts,
ColonySearch,
ColonyGetPost,
ColonyGetPostsByIds,
ColonyGetComments,
ColonyGetMe,
ColonyGetUser,
ColonyGetUsersByIds,
ColonyListColonies,
ColonyGetConversation,
ColonyGetNotifications,
Expand Down
14 changes: 9 additions & 5 deletions tests/test_async_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,37 +112,41 @@ def test_omits_retry_when_unset(self) -> None:
def test_get_tools_returns_all(self) -> None:
toolkit = AsyncColonyToolkit(api_key="col_test")
tools = toolkit.get_tools()
assert len(tools) == 31
assert len(tools) == 33
names = {t.name for t in tools}
assert "colony_create_post" in names
assert "colony_get_all_comments" in names
assert "colony_get_posts_by_ids" in names
assert "colony_get_users_by_ids" in names

def test_get_tools_read_only(self) -> None:
toolkit = AsyncColonyToolkit(api_key="col_test", read_only=True)
tools = toolkit.get_tools()
assert len(tools) == 13
assert len(tools) == 15
names = {t.name for t in tools}
assert "colony_create_post" not in names
assert "colony_get_posts_by_ids" in names
assert "colony_get_users_by_ids" in names

def test_get_tools_include_exclude(self) -> None:
toolkit = AsyncColonyToolkit(api_key="col_test")
tools = toolkit.get_tools(include=["colony_get_me"])
assert len(tools) == 1
tools = toolkit.get_tools(exclude=["colony_create_post"])
assert len(tools) == 30
assert len(tools) == 32

def test_get_tools_with_callbacks(self) -> None:
from crewai_colony.callbacks import CounterCallback

counter = CounterCallback()
toolkit = AsyncColonyToolkit(api_key="col_test", callbacks=[counter])
tools = toolkit.get_tools()
assert len(tools) == 31
assert len(tools) == 33

async def test_async_context_manager(self) -> None:
async with AsyncColonyToolkit(api_key="col_test") as toolkit:
tools = toolkit.get_tools()
assert len(tools) == 31
assert len(tools) == 33

async def test_aclose_idempotent(self) -> None:
toolkit = AsyncColonyToolkit(api_key="col_test")
Expand Down
Loading