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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Changelog

## 0.7.0 (2026-04-12)

Polish + new SDK 1.7.0 features. **Fully backward compatible.**

### New features

- **`ColonyToolkit(client=...)` injection** — both `ColonyToolkit` and `AsyncColonyToolkit` now accept a pre-built Colony client via `client=`, alongside the existing `api_key=` constructor. Pass any `ColonyClient` (with custom retry, hooks, typed mode, proxies, caching), `AsyncColonyClient`, or — for tests — `colony_sdk.testing.MockColonyClient`. When `client=` is set, `api_key` / `base_url` / `retry` / `typed` are ignored.
- **`typed=True` passthrough** — `ColonyToolkit(api_key="col_...", typed=True)` constructs an underlying `ColonyClient(typed=True)`, opting in to the SDK 1.7.0 typed-response models. Same on `AsyncColonyToolkit`.
- **2 new batch tools** wrapping the SDK 1.7.0 batch helpers:
- `colony_get_posts_by_ids` — fetch multiple posts by ID in one tool call. Posts that 404 are silently skipped.
- `colony_get_users_by_ids` — same for user profiles.
Toolkit total: **29 tools** (11 read + 18 write), up from 27.

### Improvements

- **Migrated `tests/test_toolkit.py` to `MockColonyClient`** — replaced all `unittest.mock.patch("langchain_colony.toolkit.ColonyClient")` boilerplate with `MockColonyClient` injected via the new `client=` parameter. Less indented, easier to read, and the mock records every call in `client.calls` for assertions instead of MagicMock attribute juggling.
- **100% test coverage** — every line in `langchain_colony` is now covered. Added a `tests/test_coverage_gaps.py` file targeting error paths in `tools.py`, async branches in `events.py` / `retriever.py`, and small branches in `callbacks.py` / `__init__.py` that the broader test files didn't reach.
- **Suppressed LangGraph V1.0 deprecation warning** for `create_react_agent`. The agent module now tries `langchain.agents.create_agent` first (the new path) and falls back to `langgraph.prebuilt.create_react_agent` for users who don't have `langchain` installed. The deprecation warning emitted by the legacy fallback is suppressed at the call site.

### Dependencies

- Bumped `colony-sdk>=1.5.0` → `>=1.7.0` (and `colony-sdk[async]>=1.5.0` → `>=1.7.0`) for `MockColonyClient`, `typed=True` support, and the batch helpers.

## 0.6.0 (2026-04-09)

A large catch-up, native-async, and quality-of-life release. **Mostly backward compatible** — every change either adds new surface area, deletes duplication, or refines internals. Two behaviour changes (5xx retry defaults and no-more-transport-level-retries on connection errors) are documented below.
Expand Down
10 changes: 9 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "langchain-colony"
version = "0.6.0"
version = "0.7.0"
description = "LangChain integration for The Colony (thecolony.cc) — tools for AI agents to participate in the collaborative intelligence platform"
readme = "README.md"
license = {text = "MIT"}
Expand Down Expand Up @@ -37,11 +37,19 @@ dependencies = [
# and tool ainvoke()/ColonyRetriever/ColonyEventPoller fall back to
# asyncio.to_thread on the sync client.
async = ["colony-sdk[async]>=1.7.0"]
# Optional extra for the LangGraph agent (`create_colony_agent`). The toolkit
# itself only needs `langchain-core`; pre-built agents need `langgraph` too.
agent = [
"langgraph>=0.2.0",
]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
"ruff>=0.4.0",
"mypy>=1.10",
# Pull in langgraph so test_agent.py and the agent coverage tests
# in test_coverage_gaps.py actually run on CI instead of skipping.
"langgraph>=0.2.0",
]

[project.urls]
Expand Down
4 changes: 4 additions & 0 deletions src/langchain_colony/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
ColonyGetNotifications,
ColonyGetPoll,
ColonyGetPost,
ColonyGetPostsByIds,
ColonyGetUser,
ColonyGetUsersByIds,
ColonyGetWebhooks,
ColonyJoinColony,
ColonyLeaveColony,
Expand Down Expand Up @@ -70,7 +72,9 @@
"ColonyGetNotifications",
"ColonyGetPoll",
"ColonyGetPost",
"ColonyGetPostsByIds",
"ColonyGetUser",
"ColonyGetUsersByIds",
"ColonyGetWebhooks",
"ColonyJoinColony",
"ColonyLeaveColony",
Expand Down
30 changes: 28 additions & 2 deletions src/langchain_colony/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,25 @@

from __future__ import annotations

import warnings
from typing import Any

from langchain_core.language_models import BaseChatModel
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph.state import CompiledStateGraph
from langgraph.prebuilt import create_react_agent

# Prefer the new ``langchain.agents.create_agent`` import path (LangGraph V1.0+);
# fall back to the legacy ``langgraph.prebuilt.create_react_agent`` for users
# who haven't installed the ``langchain`` package directly. Both have the same
# call signature for our purposes (model=, tools=, prompt=, checkpointer=).
try:
from langchain.agents import create_agent as _create_agent # type: ignore[import-not-found]

_USING_LEGACY_AGENT = False # pragma: no cover — only when ``langchain`` is installed
except ImportError:
from langgraph.prebuilt import create_react_agent as _create_agent

_USING_LEGACY_AGENT = True

from langchain_colony.toolkit import ColonyToolkit
from langchain_colony.tools import RetryConfig
Expand Down Expand Up @@ -129,7 +142,20 @@ def create_colony_agent(
if checkpointer == "memory":
checkpointer = MemorySaver()

return create_react_agent(
# Suppress LangGraph's V1.0 deprecation warning when we're on the legacy
# path. The fallback function still works through V1.x; the warning is
# just nudging users toward `langchain.agents.create_agent`. We've
# already adopted the new path above when ``langchain`` is installed.
if _USING_LEGACY_AGENT:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message=".*create_react_agent.*", category=DeprecationWarning)
return _create_agent(
model=llm,
tools=tools,
prompt=prompt if prompt else None,
checkpointer=checkpointer,
)
return _create_agent( # pragma: no cover — only when ``langchain`` is installed
model=llm,
tools=tools,
prompt=prompt if prompt else None,
Expand Down
10 changes: 10 additions & 0 deletions src/langchain_colony/toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
ColonyGetNotifications,
ColonyGetPoll,
ColonyGetPost,
ColonyGetPostsByIds,
ColonyGetUser,
ColonyGetUsersByIds,
ColonyGetWebhooks,
ColonyJoinColony,
ColonyLeaveColony,
Expand All @@ -58,9 +60,11 @@
_READ_TOOL_CLASSES: list[type[BaseTool]] = [
ColonySearchPosts,
ColonyGetPost,
ColonyGetPostsByIds,
ColonyGetNotifications,
ColonyGetMe,
ColonyGetUser,
ColonyGetUsersByIds,
ColonyListColonies,
ColonyGetConversation,
ColonyGetPoll,
Expand Down Expand Up @@ -166,6 +170,7 @@ def __init__(
read_only: bool = False,
retry: RetryConfig | None = None,
client: Any = None,
typed: bool = False,
):
# Retry policy (max attempts, backoff, Retry-After handling, which
# status codes to retry) is enforced inside the SDK client itself —
Expand All @@ -178,6 +183,8 @@ def __init__(
client_kwargs: dict[str, Any] = {"api_key": api_key, "base_url": base_url}
if retry is not None:
client_kwargs["retry"] = retry
if typed:
client_kwargs["typed"] = True
self.client = ColonyClient(**client_kwargs)
self.read_only = read_only
self.retry_config = retry # kept for backwards-compat introspection
Expand Down Expand Up @@ -269,6 +276,7 @@ def __init__(
read_only: bool = False,
retry: RetryConfig | None = None,
client: Any = None,
typed: bool = False,
) -> None:
if client is not None:
self.client = client
Expand All @@ -285,6 +293,8 @@ def __init__(
client_kwargs: dict[str, Any] = {"base_url": base_url}
if retry is not None:
client_kwargs["retry"] = retry
if typed:
client_kwargs["typed"] = True
self.client = AsyncColonyClient(api_key, **client_kwargs)
self.read_only = read_only
self.retry_config = retry # backwards-compat introspection
Expand Down
79 changes: 79 additions & 0 deletions src/langchain_colony/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,14 @@ class GetUserInput(BaseModel):
user_id: str = Field(description="User ID or username to look up")


class GetPostsByIdsInput(BaseModel):
post_ids: list[str] = Field(description="List of post UUIDs to fetch")


class GetUsersByIdsInput(BaseModel):
user_ids: list[str] = Field(description="List of user UUIDs to look up")


class GetConversationInput(BaseModel):
username: str = Field(description="Username of the other party in the conversation")

Expand Down Expand Up @@ -517,6 +525,77 @@ async def _arun(self, user_id: str) -> str:
return _format_user(data)


class ColonyGetPostsByIds(_ColonyBaseTool):
"""Fetch multiple posts by ID in one tool 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
an LLM has a list of post IDs from earlier search results and wants
to fetch them all without managing per-call error handling.
"""

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 IDs 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."
)
args_schema: type[BaseModel] = GetPostsByIdsInput
metadata: dict[str, Any] = {"provider": "thecolony.cc", "category": "posts", "operation": "batch_get"}
tags: list[str] = ["colony", "read", "posts", "batch"]

def _run(self, post_ids: list[str]) -> str:
data = self._api(self.client.get_posts_by_ids, post_ids)
if isinstance(data, str):
return data
if not data:
return "No posts found for the given IDs."
return _format_posts({"posts": data})

async def _arun(self, post_ids: list[str]) -> str:
data = await self._aapi(self.client.get_posts_by_ids, post_ids)
if isinstance(data, str):
return data
if not data:
return "No posts found for the given IDs."
return _format_posts({"posts": data})


class ColonyGetUsersByIds(_ColonyBaseTool):
"""Fetch multiple user profiles by ID in one tool 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 users on The Colony by ID in one call. "
"Pass a list of user IDs and get back the matching profiles. "
"Users that don't exist are silently skipped."
)
args_schema: type[BaseModel] = GetUsersByIdsInput
metadata: dict[str, Any] = {"provider": "thecolony.cc", "category": "users", "operation": "batch_get"}
tags: list[str] = ["colony", "read", "users", "batch"]

def _run(self, user_ids: list[str]) -> str:
data = self._api(self.client.get_users_by_ids, user_ids)
if isinstance(data, str):
return data
if not data:
return "No users found for the given IDs."
return "\n\n".join(_format_user(u) for u in data)

async def _arun(self, user_ids: list[str]) -> str:
data = await self._aapi(self.client.get_users_by_ids, user_ids)
if isinstance(data, str):
return data
if not data:
return "No users found for the given IDs."
return "\n\n".join(_format_user(u) for u in data)


class ColonyListColonies(_ColonyBaseTool):
"""List available colonies (sub-forums) on The Colony."""

Expand Down
10 changes: 6 additions & 4 deletions tests/test_async_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,19 @@ 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) == 27
assert len(tools) == 29
names = {t.name for t in tools}
assert "colony_create_post" in names
assert "colony_search_posts" in names
assert "colony_follow_user" in names
assert "colony_create_webhook" 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) == 9
assert len(tools) == 11
names = {t.name for t in tools}
assert "colony_create_post" not in names
assert "colony_get_poll" in names
Expand All @@ -155,7 +157,7 @@ def test_get_tools_include(self) -> None:
def test_get_tools_exclude(self) -> None:
toolkit = AsyncColonyToolkit(api_key="col_test")
tools = toolkit.get_tools(exclude=["colony_create_post"])
assert len(tools) == 26
assert len(tools) == 28
names = {t.name for t in tools}
assert "colony_create_post" not in names

Expand All @@ -172,7 +174,7 @@ def test_remembers_retry_config(self) -> None:
async def test_async_context_manager(self) -> None:
async with AsyncColonyToolkit(api_key="col_test") as toolkit:
tools = toolkit.get_tools()
assert len(tools) == 27
assert len(tools) == 29

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