From 899bdc5f8c9a9e3dd9501eaf2e5b0e9a3580b927 Mon Sep 17 00:00:00 2001 From: Faouzi Braza Date: Sat, 27 Sep 2025 18:32:55 +0200 Subject: [PATCH 1/4] refactor: with pydantic ai --- app/tools/registry.py | 14 +-- app/tools/slack.py | 20 ++- pyproject.toml | 1 + tests/test_ava_slack_post_message.py | 74 +++++++++++ tests/test_health_ava.py | 26 ---- tests/test_send_tasks.py | 38 ------ uv.lock | 175 ++++++++++++++++++++++++++- 7 files changed, 269 insertions(+), 79 deletions(-) create mode 100644 tests/test_ava_slack_post_message.py delete mode 100644 tests/test_health_ava.py delete mode 100644 tests/test_send_tasks.py diff --git a/app/tools/registry.py b/app/tools/registry.py index 08b8fa3..36e3325 100644 --- a/app/tools/registry.py +++ b/app/tools/registry.py @@ -1,13 +1,5 @@ -from collections.abc import Callable +from pydantic_ai.toolsets import FunctionToolset -from slack_sdk.web.slack_response import SlackResponse +from app.tools.slack import slack_toolset -from app.models.tools import SlackChatPostMessageParams -from app.tools.slack import post_message - -TOOLS: dict[str, dict[str, type | Callable[..., SlackResponse]]] = { - "slack.chat.postMessage": { - "schema": SlackChatPostMessageParams, - "fn": post_message, - }, -} +TOOLS: dict[str, FunctionToolset] = {"slack.tools": slack_toolset} diff --git a/app/tools/slack.py b/app/tools/slack.py index 5af06e2..ddeb671 100644 --- a/app/tools/slack.py +++ b/app/tools/slack.py @@ -1,9 +1,23 @@ +from dataclasses import dataclass + +from pydantic_ai import RunContext +from pydantic_ai.toolsets import FunctionToolset from slack_sdk import WebClient from slack_sdk.web.slack_response import SlackResponse from app.models.tools import SlackChatPostMessageParams -def post_message(token: str, params: SlackChatPostMessageParams) -> SlackResponse: - client = WebClient(token=token) - return client.chat_postMessage(channel=params.channel, text=params.text) +@dataclass +class Deps: + client: WebClient + + +slack_toolset = FunctionToolset() + + +@slack_toolset.tool(name="slack.chat.postMessage") +def post_message( + ctx: RunContext[Deps], params: SlackChatPostMessageParams +) -> SlackResponse: + return ctx.deps.client.chat_postMessage(channel=params.channel, text=params.text) diff --git a/pyproject.toml b/pyproject.toml index 384f2aa..0c681d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,4 +20,5 @@ dev = [ "pre-commit", "coverage", "ruff", + "ipython>=8.37.0", ] diff --git a/tests/test_ava_slack_post_message.py b/tests/test_ava_slack_post_message.py new file mode 100644 index 0000000..bd9e236 --- /dev/null +++ b/tests/test_ava_slack_post_message.py @@ -0,0 +1,74 @@ +import os + +import pytest +from dotenv import load_dotenv +from pydantic_ai import Agent, RunContext, RunUsage +from pydantic_ai.models.test import TestModel +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError + +from app.models import tools +from app.tools import slack + +_ = load_dotenv() + + +def test_health_ava_can_post_message(): + token = os.environ.get("SLACK_BOT_TOKEN") + if not token: + pytest.skip("SLACK_BOT_TOKEN not set; live Slack test skipped") + + client = WebClient(token=token) + message_text = "AVA backend live smoke :rocket:" + + try: + response = client.chat_postMessage(channel="sandbox", text=message_text) + except SlackApiError as exc: + pytest.fail(f"Slack API error: {exc.response['error']}") + + assert response["ok"] is True + assert response["message"]["text"] == message_text # type: ignore + assert "ts" in response + + +@pytest.mark.parametrize( + "channel,text,token", + [ + ( + "sandbox", + "Hello, AVA backend live smoke :rocket:", + os.environ.get("SLACK_BOT_TOKEN"), + ), + ( + "general", + "Hello, User is live smoke :rocket:", + os.environ.get("SLACK_USER_TOKEN"), + ), + ], +) +def test_post_message_with_ctx(channel: str, text: str, token: str | None): + if token is not None: + client = WebClient(token=token) + ctx = RunContext( + deps=slack.Deps(client=client), + model=TestModel(), + usage=RunUsage(), + ) + tool_params = tools.SlackChatPostMessageParams(channel=channel, text=text) + try: + response = slack.post_message(ctx=ctx, params=tool_params) # type: ignore + except SlackApiError as exc: + pytest.fail(f"Slack API error: {exc.response['error']}") + + assert response["ok"] is True + assert response["message"]["text"] == text + assert "ts" in response + + +def test_slack_tools_are_synced(): + test_model = TestModel() + agent = Agent(test_model, toolsets=[slack.slack_toolset]) + _ = agent.run_sync("What tools are available?") + assert [ + t.name for t in test_model.last_model_request_parameters.function_tools + ] == ["slack.chat.postMessage"] diff --git a/tests/test_health_ava.py b/tests/test_health_ava.py deleted file mode 100644 index 3a3ec2b..0000000 --- a/tests/test_health_ava.py +++ /dev/null @@ -1,26 +0,0 @@ -import os - -import pytest -from dotenv import load_dotenv -from slack_sdk import WebClient -from slack_sdk.errors import SlackApiError - - -def test_health_ava_can_post_message(): - _ = load_dotenv() - - token = os.environ.get("SLACK_BOT_TOKEN") - if not token: - pytest.skip("SLACK_BOT_TOKEN not set; live Slack test skipped") - - client = WebClient(token=token) - message_text = "AVA backend live smoke :rocket:" - - try: - response = client.chat_postMessage(channel="sandbox", text=message_text) - except SlackApiError as exc: - pytest.fail(f"Slack API error: {exc.response['error']}") - - assert response["ok"] is True - assert response["message"]["text"] == message_text - assert "ts" in response diff --git a/tests/test_send_tasks.py b/tests/test_send_tasks.py deleted file mode 100644 index 52f3804..0000000 --- a/tests/test_send_tasks.py +++ /dev/null @@ -1,38 +0,0 @@ -import os - -import pytest -from dotenv import load_dotenv -from slack_sdk.errors import SlackApiError - -from app.models import tools -from app.tools import slack - -_ = load_dotenv() - - -@pytest.mark.parametrize( - "channel,text,token", - [ - ( - "sandbox", - "Hello, AVA backend live smoke :rocket:", - os.environ.get("SLACK_BOT_TOKEN"), - ), - ( - "general", - "Hello, User is live smoke :rocket:", - os.environ.get("SLACK_USER_TOKEN"), - ), - ], -) -def test_post_message_with_tool(channel: str, text: str, token: str | None): - if token is not None: - tool_params = tools.SlackChatPostMessageParams(channel=channel, text=text) - try: - response = slack.post_message(token=token, params=tool_params) - except SlackApiError as exc: - pytest.fail(f"Slack API error: {exc.response['error']}") - - assert response["ok"] is True - assert response["message"]["text"] == text - assert "ts" in response diff --git a/uv.lock b/uv.lock index fbeeb32..465fb8e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] [[package]] name = "ag-ui-protocol" @@ -174,6 +178,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/da/e42d7a9d8dd33fa775f467e4028a47936da2f01e4b0e561f9ba0d74cb0ca/argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591", size = 43708, upload-time = "2025-04-03T04:57:01.591Z" }, ] +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, +] + [[package]] name = "async-timeout" version = "5.0.1" @@ -209,6 +222,8 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "coverage" }, + { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pre-commit" }, { name = "pytest" }, { name = "ruff" }, @@ -228,6 +243,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "coverage" }, + { name = "ipython", specifier = ">=8.37.0" }, { name = "pre-commit" }, { name = "pytest" }, { name = "ruff" }, @@ -492,6 +508,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, ] +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + [[package]] name = "distlib" version = "0.4.0" @@ -533,7 +558,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -933,6 +958,80 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/66/7f8c48009c72d73bc6bbe6eb87ac838d6a526146f7dab14af671121eb379/invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820", size = 160274, upload-time = "2023-07-12T18:05:16.294Z" }, ] +[[package]] +name = "ipython" +version = "8.37.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864, upload-time = "2025-05-31T16:39:06.38Z" }, +] + +[[package]] +name = "ipython" +version = "9.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.11'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/71/a86262bf5a68bf211bcc71fe302af7e05f18a2852fdc610a854d20d085e6/ipython-9.5.0.tar.gz", hash = "sha256:129c44b941fe6d9b82d36fc7a7c18127ddb1d6f02f78f867f402e2e3adde3113", size = 4389137, upload-time = "2025-08-29T12:15:21.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl", hash = "sha256:88369ffa1d5817d609120daa523a6da06d02518e582347c29f8451732a9c5e72", size = 612426, upload-time = "2025-08-29T12:15:18.866Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + [[package]] name = "jiter" version = "0.11.0" @@ -1087,6 +1186,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, +] + [[package]] name = "mcp" version = "1.14.1" @@ -1409,6 +1520,27 @@ 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 = "parso" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + [[package]] name = "platformdirs" version = "4.4.0" @@ -1558,6 +1690,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823, upload-time = "2025-05-28T23:51:58.157Z" }, ] +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + [[package]] name = "pyasn1" version = "0.6.1" @@ -2211,6 +2361,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, ] +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + [[package]] name = "starlette" version = "0.48.0" @@ -2329,6 +2493,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + [[package]] name = "types-protobuf" version = "6.32.1.20250918" From dc5e7c7acefc4a66c254931467502e9d00a0097e Mon Sep 17 00:00:00 2001 From: Faouzi Braza Date: Sat, 27 Sep 2025 18:54:03 +0200 Subject: [PATCH 2/4] tests: registry tool --- tests/test_ava_slack_post_message.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_ava_slack_post_message.py b/tests/test_ava_slack_post_message.py index bd9e236..9d8bfb2 100644 --- a/tests/test_ava_slack_post_message.py +++ b/tests/test_ava_slack_post_message.py @@ -2,7 +2,7 @@ import pytest from dotenv import load_dotenv -from pydantic_ai import Agent, RunContext, RunUsage +from pydantic_ai import Agent, RunContext, RunUsage, models from pydantic_ai.models.test import TestModel from slack_sdk import WebClient from slack_sdk.errors import SlackApiError @@ -56,7 +56,7 @@ def test_post_message_with_ctx(channel: str, text: str, token: str | None): ) tool_params = tools.SlackChatPostMessageParams(channel=channel, text=text) try: - response = slack.post_message(ctx=ctx, params=tool_params) # type: ignore + response = slack.post_message(ctx=ctx, params=tool_params) except SlackApiError as exc: pytest.fail(f"Slack API error: {exc.response['error']}") @@ -66,9 +66,13 @@ def test_post_message_with_ctx(channel: str, text: str, token: str | None): def test_slack_tools_are_synced(): - test_model = TestModel() - agent = Agent(test_model, toolsets=[slack.slack_toolset]) - _ = agent.run_sync("What tools are available?") + models.ALLOW_MODEL_REQUESTS = False + token = os.environ.get("SLACK_BOT_TOKEN") + client = WebClient(token=token) + test_model = TestModel(call_tools=[]) + agent = Agent(test_model, toolsets=[slack.slack_toolset], deps_type=slack.Deps) + _ = agent.run_sync("What tools are available?", deps=slack.Deps(client=client)) + assert [ t.name for t in test_model.last_model_request_parameters.function_tools ] == ["slack.chat.postMessage"] From cd64220f7f92ca15d1aa1898de50ca4720b1375e Mon Sep 17 00:00:00 2001 From: Faouzi Braza Date: Sat, 27 Sep 2025 20:00:02 +0200 Subject: [PATCH 3/4] docs & refactor: remove plan as not needed, add docstring to help agent tool selection --- app/models/plan.py | 18 ------------------ app/models/tools.py | 4 ++++ app/tools/slack.py | 3 +++ 3 files changed, 7 insertions(+), 18 deletions(-) delete mode 100644 app/models/plan.py diff --git a/app/models/plan.py b/app/models/plan.py deleted file mode 100644 index 6a83817..0000000 --- a/app/models/plan.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Literal - -from pydantic import BaseModel - -from app.models.tools import SlackChatPostMessageParams - - -class Step(BaseModel): - """Single tool invocation within a plan.""" - - tool: Literal["slack.chat.postMessage"] - params: SlackChatPostMessageParams - - -class Plan(BaseModel): - """Sequence of tool steps produced by the planner.""" - - steps: list[Step] diff --git a/app/models/tools.py b/app/models/tools.py index 938b003..bae5a14 100644 --- a/app/models/tools.py +++ b/app/models/tools.py @@ -2,5 +2,9 @@ class SlackChatPostMessageParams(BaseModel): + """ + Parameters for the slack.chat.postMessage tool + """ + channel: str text: str diff --git a/app/tools/slack.py b/app/tools/slack.py index ddeb671..ad51ad0 100644 --- a/app/tools/slack.py +++ b/app/tools/slack.py @@ -20,4 +20,7 @@ class Deps: def post_message( ctx: RunContext[Deps], params: SlackChatPostMessageParams ) -> SlackResponse: + """ + Use this function to post a message in the specified channel + """ return ctx.deps.client.chat_postMessage(channel=params.channel, text=params.text) From d22a7e628d0d3eeb9b1dee2e4292db2779d739fc Mon Sep 17 00:00:00 2001 From: Faouzi Braza Date: Mon, 29 Sep 2025 10:46:18 +0200 Subject: [PATCH 4/4] removed unused import --- tests/test_ava_slack_post_message.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_ava_slack_post_message.py b/tests/test_ava_slack_post_message.py index 9d8bfb2..dfdb955 100644 --- a/tests/test_ava_slack_post_message.py +++ b/tests/test_ava_slack_post_message.py @@ -2,7 +2,7 @@ import pytest from dotenv import load_dotenv -from pydantic_ai import Agent, RunContext, RunUsage, models +from pydantic_ai import Agent, RunContext, RunUsage from pydantic_ai.models.test import TestModel from slack_sdk import WebClient from slack_sdk.errors import SlackApiError @@ -66,7 +66,6 @@ def test_post_message_with_ctx(channel: str, text: str, token: str | None): def test_slack_tools_are_synced(): - models.ALLOW_MODEL_REQUESTS = False token = os.environ.get("SLACK_BOT_TOKEN") client = WebClient(token=token) test_model = TestModel(call_tools=[])