From ce3f3d14d29d67c34f0b1fa4468cc489b959b807 Mon Sep 17 00:00:00 2001 From: Jason Hwee <1216418+hweej@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:21:31 -0400 Subject: [PATCH 1/5] feat(agent): add anthropic[bedrock] (boto3) runtime dependency --- packages/cell-explorer-agent/pyproject.toml | 2 +- .../tests/test_llm_client_transport.py | 7 +++ uv.lock | 59 ++++++++++++++++++- 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 packages/cell-explorer-agent/tests/test_llm_client_transport.py diff --git a/packages/cell-explorer-agent/pyproject.toml b/packages/cell-explorer-agent/pyproject.toml index 41f726d..0c374ff 100644 --- a/packages/cell-explorer-agent/pyproject.toml +++ b/packages/cell-explorer-agent/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "LLM chat agent library for single-cell zarr datasets" requires-python = ">=3.12" dependencies = [ - "anthropic>=0.40", + "anthropic[bedrock]>=0.40", "pydantic>=2.0", "pydantic-settings>=2.8", "numpy>=1.26", diff --git a/packages/cell-explorer-agent/tests/test_llm_client_transport.py b/packages/cell-explorer-agent/tests/test_llm_client_transport.py new file mode 100644 index 0000000..0fcf7b3 --- /dev/null +++ b/packages/cell-explorer-agent/tests/test_llm_client_transport.py @@ -0,0 +1,7 @@ +"""AnthropicLLMClient transport selection + Bedrock runtime deps.""" + + +def test_bedrock_runtime_dep_importable(): + # anthropic[bedrock] provides boto3, required for Bedrock SigV4/cred resolution + # at request time (construction alone does not import it). + import boto3 # noqa: F401 diff --git a/uv.lock b/uv.lock index 623cb8d..c268969 100644 --- a/uv.lock +++ b/uv.lock @@ -219,6 +219,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/5a/72f33204064b6e87601a71a6baf8d855769f8a0c1eaae8d06a1094872371/anthropic-0.96.0-py3-none-any.whl", hash = "sha256:9a6e335a354602a521cd9e777e92bfd46ba6e115bf9bbfe6135311e8fb2015b2", size = 635930, upload-time = "2026-04-16T14:28:01.436Z" }, ] +[package.optional-dependencies] +bedrock = [ + { name = "boto3" }, + { name = "botocore" }, +] + [[package]] name = "anyio" version = "4.13.0" @@ -422,12 +428,40 @@ css = [ { name = "tinycss2" }, ] +[[package]] +name = "boto3" +version = "1.43.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/5b/e91af651bf4e902f86b9e2f8bc09ca20dbd1ad3c1e21b70bf34651cf3cee/boto3-1.43.41.tar.gz", hash = "sha256:0f56811f13677bfb4542daa0cce8532c95d9afd27b4ba7b681af36a0568624ad", size = 112677, upload-time = "2026-07-06T19:39:38.788Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/4a/babb2de16f6ff0688697d2eac7d95566151c255f6b08f547306f57dabfc2/boto3-1.43.41-py3-none-any.whl", hash = "sha256:f48f862d2720ea9203ed2d842d436b8eb2d459ea31654a7ad7c0756fdf36c6b2", size = 140029, upload-time = "2026-07-06T19:39:37.125Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/a1/48a0f38b0cac8196764607c0bca7e3ca50d3cffc825087b743d3635413f2/botocore-1.43.41.tar.gz", hash = "sha256:27627d79af0df7dcb7ecf78d8d3d1310da09a5e9460be30bf759f1c2ed095ee8", size = 15647567, upload-time = "2026-07-06T19:39:27.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/4f/19e0b97ce1801c66a4d1d35117f36240aa20f864338f093fccc23873a231/botocore-1.43.41-py3-none-any.whl", hash = "sha256:0cc6e79b30a2a98374f16a31cd9c7a9106a51b60650bd8c34cc8223f58ae6b8d", size = 15331199, upload-time = "2026-07-06T19:39:23.694Z" }, +] + [[package]] name = "cell-explorer-agent" version = "0.1.0" source = { editable = "packages/cell-explorer-agent" } dependencies = [ - { name = "anthropic" }, + { name = "anthropic", extra = ["bedrock"] }, { name = "httpx" }, { name = "langfuse" }, { name = "numpy" }, @@ -450,7 +484,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "anthropic", specifier = ">=0.40" }, + { name = "anthropic", extras = ["bedrock"], specifier = ">=0.40" }, { name = "httpx", specifier = ">=0.28" }, { name = "langfuse", specifier = ">=3.0,<4.0" }, { name = "numpy", specifier = ">=1.26" }, @@ -1584,6 +1618,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + [[package]] name = "joblib" version = "1.5.3" @@ -3541,6 +3584,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, ] +[[package]] +name = "s3transfer" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/94/dcdaeb1713cab9c84def276cfac7388b17c7d9855bbcfe88d77e4dbafd44/s3transfer-0.19.0.tar.gz", hash = "sha256:ce436931687addc4c1712d52d40b32f53e88315723f107ffa20ba82b05a0f685", size = 165171, upload-time = "2026-06-16T19:44:51.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/5f/4c174edad94f82de888ac00a5ddd8d07b35609b6c94f0bdf4d74af57703e/s3transfer-0.19.0-py3-none-any.whl", hash = "sha256:777cc2415536f1debadb5c2ef7779275d0fc0fe0e042411cdd6caebeb2685262", size = 90101, upload-time = "2026-06-16T19:44:50.439Z" }, +] + [[package]] name = "scanpy" version = "1.11.5" From 9bddcd9cba782c0e366d8b8cda73efa0f2efa56b Mon Sep 17 00:00:00 2001 From: Jason Hwee <1216418+hweej@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:22:30 -0400 Subject: [PATCH 2/5] feat(agent): add CHAT_BEDROCK_REGION config field --- .../cell-explorer-agent/src/cell_explorer_agent/config.py | 1 + packages/cell-explorer-agent/tests/test_config.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/packages/cell-explorer-agent/src/cell_explorer_agent/config.py b/packages/cell-explorer-agent/src/cell_explorer_agent/config.py index 3360cd9..7b55662 100644 --- a/packages/cell-explorer-agent/src/cell_explorer_agent/config.py +++ b/packages/cell-explorer-agent/src/cell_explorer_agent/config.py @@ -13,6 +13,7 @@ class AgentConfig(BaseSettings): llm_transport: Literal["anthropic", "bedrock", "vertex"] = "anthropic" llm_model: str = "claude-sonnet-4-6" + bedrock_region: str = "us-east-1" tool_result_max_bytes: int = Field(default=32_768, ge=1) filter_ids_max: int = Field(default=100_000, ge=1) diff --git a/packages/cell-explorer-agent/tests/test_config.py b/packages/cell-explorer-agent/tests/test_config.py index 52b8f53..99ae73f 100644 --- a/packages/cell-explorer-agent/tests/test_config.py +++ b/packages/cell-explorer-agent/tests/test_config.py @@ -98,3 +98,9 @@ def test_langfuse_base_url_default(monkeypatch): from cell_explorer_agent.config import AgentConfig cfg = AgentConfig() assert cfg.langfuse_base_url == "https://us.cloud.langfuse.com" + + +def test_bedrock_region_default_and_override(monkeypatch): + assert AgentConfig().bedrock_region == "us-east-1" + monkeypatch.setenv("CHAT_BEDROCK_REGION", "us-west-2") + assert AgentConfig().bedrock_region == "us-west-2" From 9bde0e48946484e8a70ffd6c93af0007d1e50966 Mon Sep 17 00:00:00 2001 From: Jason Hwee <1216418+hweej@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:25:25 -0400 Subject: [PATCH 3/5] feat(agent): pass CHAT_BEDROCK_REGION to AsyncAnthropicBedrock --- .../src/cell_explorer_agent/llm/anthropic.py | 6 +++-- .../tests/test_llm_client_transport.py | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/cell-explorer-agent/src/cell_explorer_agent/llm/anthropic.py b/packages/cell-explorer-agent/src/cell_explorer_agent/llm/anthropic.py index 93ee55c..91364d7 100644 --- a/packages/cell-explorer-agent/src/cell_explorer_agent/llm/anthropic.py +++ b/packages/cell-explorer-agent/src/cell_explorer_agent/llm/anthropic.py @@ -89,13 +89,15 @@ def neutral_messages_to_anthropic( class AnthropicLLMClient(LLMClient): """Anthropic SDK adapter. Supports direct/Bedrock/Vertex via transport arg.""" - def __init__(self, *, transport: Transport = "anthropic") -> None: + def __init__( + self, *, transport: Transport = "anthropic", bedrock_region: str = "us-east-1" + ) -> None: import anthropic if transport == "anthropic": self._client = anthropic.AsyncAnthropic() elif transport == "bedrock": - self._client = anthropic.AsyncAnthropicBedrock() + self._client = anthropic.AsyncAnthropicBedrock(aws_region=bedrock_region) elif transport == "vertex": self._client = anthropic.AsyncAnthropicVertex() else: diff --git a/packages/cell-explorer-agent/tests/test_llm_client_transport.py b/packages/cell-explorer-agent/tests/test_llm_client_transport.py index 0fcf7b3..57db7dc 100644 --- a/packages/cell-explorer-agent/tests/test_llm_client_transport.py +++ b/packages/cell-explorer-agent/tests/test_llm_client_transport.py @@ -1,7 +1,29 @@ """AnthropicLLMClient transport selection + Bedrock runtime deps.""" +import anthropic + +from cell_explorer_agent import AnthropicLLMClient + def test_bedrock_runtime_dep_importable(): # anthropic[bedrock] provides boto3, required for Bedrock SigV4/cred resolution # at request time (construction alone does not import it). import boto3 # noqa: F401 + + +def test_bedrock_transport_uses_region(): + client = AnthropicLLMClient(transport="bedrock", bedrock_region="us-east-1") + assert isinstance(client._client, anthropic.AsyncAnthropicBedrock) + assert client._client.aws_region == "us-east-1" + + +def test_bedrock_transport_region_override(): + client = AnthropicLLMClient(transport="bedrock", bedrock_region="eu-west-1") + assert client._client.aws_region == "eu-west-1" + + +def test_anthropic_transport_is_default(monkeypatch): + # AsyncAnthropic() needs an api key present to construct + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test") + client = AnthropicLLMClient() + assert isinstance(client._client, anthropic.AsyncAnthropic) From d533e1b16e6c1245cdef64169b4a94f42e20f35d Mon Sep 17 00:00:00 2001 From: Jason Hwee <1216418+hweej@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:26:26 -0400 Subject: [PATCH 4/5] feat(api): pass bedrock_region when constructing the LLM client --- packages/api/src/cell_explorer_api/services/chat_session.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/api/src/cell_explorer_api/services/chat_session.py b/packages/api/src/cell_explorer_api/services/chat_session.py index 4be5f58..12249d1 100644 --- a/packages/api/src/cell_explorer_api/services/chat_session.py +++ b/packages/api/src/cell_explorer_api/services/chat_session.py @@ -201,7 +201,10 @@ async def make_chat_agent( # 6. Construct LLMClient if not provided if llm is None: - llm = AnthropicLLMClient(transport=agent_config.llm_transport) + llm = AnthropicLLMClient( + transport=agent_config.llm_transport, + bedrock_region=agent_config.bedrock_region, + ) # 7. Return the wired agent return ChatAgent(llm=llm, catalog=catalog, dataset_ctx=ctx, config=agent_config) From 7f1d40244fa76d6b6abc76a8304ce6811b04fd9f Mon Sep 17 00:00:00 2001 From: Jason Hwee <1216418+hweej@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:03:37 -0400 Subject: [PATCH 5/5] test(agent): harden bedrock region tests (non-default region, delenv, defaults snapshot) --- packages/cell-explorer-agent/tests/test_config.py | 7 ++++++- .../cell-explorer-agent/tests/test_llm_client_transport.py | 6 ++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/cell-explorer-agent/tests/test_config.py b/packages/cell-explorer-agent/tests/test_config.py index 99ae73f..e2a1894 100644 --- a/packages/cell-explorer-agent/tests/test_config.py +++ b/packages/cell-explorer-agent/tests/test_config.py @@ -8,6 +8,7 @@ def test_defaults(): cfg = AgentConfig() assert cfg.llm_transport == "anthropic" assert cfg.llm_model == "claude-sonnet-4-6" + assert cfg.bedrock_region == "us-east-1" assert cfg.tool_result_max_bytes == 32_768 assert cfg.filter_ids_max == 100_000 assert cfg.max_tool_calls_per_turn == 8 @@ -100,7 +101,11 @@ def test_langfuse_base_url_default(monkeypatch): assert cfg.langfuse_base_url == "https://us.cloud.langfuse.com" -def test_bedrock_region_default_and_override(monkeypatch): +def test_bedrock_region_default(monkeypatch): + monkeypatch.delenv("CHAT_BEDROCK_REGION", raising=False) assert AgentConfig().bedrock_region == "us-east-1" + + +def test_bedrock_region_override(monkeypatch): monkeypatch.setenv("CHAT_BEDROCK_REGION", "us-west-2") assert AgentConfig().bedrock_region == "us-west-2" diff --git a/packages/cell-explorer-agent/tests/test_llm_client_transport.py b/packages/cell-explorer-agent/tests/test_llm_client_transport.py index 57db7dc..271720a 100644 --- a/packages/cell-explorer-agent/tests/test_llm_client_transport.py +++ b/packages/cell-explorer-agent/tests/test_llm_client_transport.py @@ -12,9 +12,11 @@ def test_bedrock_runtime_dep_importable(): def test_bedrock_transport_uses_region(): - client = AnthropicLLMClient(transport="bedrock", bedrock_region="us-east-1") + # Non-default region so this fails if aws_region were dropped from the + # constructor call (the SDK would otherwise fall back to "us-east-1"). + client = AnthropicLLMClient(transport="bedrock", bedrock_region="ap-southeast-1") assert isinstance(client._client, anthropic.AsyncAnthropicBedrock) - assert client._client.aws_region == "us-east-1" + assert client._client.aws_region == "ap-southeast-1" def test_bedrock_transport_region_override():