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
1 change: 1 addition & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ updates:
directories:
- "/"
- "/openai-agents-sdk"
- "/pydantic-ai"
schedule:
interval: "daily"
cooldown:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
matrix:
directory:
- openai-agents-sdk
- pydantic-ai
defaults:
run:
working-directory: ${{ matrix.directory }}
Expand All @@ -43,6 +44,7 @@ jobs:
matrix:
directory:
- openai-agents-sdk
- pydantic-ai
defaults:
run:
working-directory: ${{ matrix.directory }}
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Guidance for coding agents working in this repository.
A monorepo for a **Coding Agent** sample for Slack, built with Bolt for Python. Each framework lives in its own directory as a standalone sample:

- `openai-agents-sdk/` — built with the **OpenAI Agents SDK**
- `pydantic-ai/` — built with **Pydantic AI**

Implementations share a structure and user experience, not code. Keep each one obvious on its own, written in its framework's idiom, rather than adding shared helpers.

Expand All @@ -32,9 +33,10 @@ pytest
```
.github/ # Shared CI workflows and dependabot config
openai-agents-sdk/ # OpenAI Agents SDK implementation
pydantic-ai/ # Pydantic AI implementation
```

CI runs ruff and pytest against each directory with a matrix in `.github/workflows/ruff.yml` and `.github/workflows/tests.yml`. Dependabot watches each directory separately. When adding an implementation, add its directory to both workflow matrices, to `.github/dependabot.yml`, and to the framework table in the root `README.md`.
CI runs ruff and pytest against each directory with a matrix in `.github/workflows/ci.yml`. Dependabot watches each directory separately. When adding an implementation, add its directory to both job matrices, to `.github/dependabot.yml`, and to the framework table in the root `README.md`.

## Architecture

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ A sample Slack agent that showcases [Slack Code](https://docs.slack.dev/ai/slack
| App | Directory | Get started | Framework |
| --------------------- | -------------------- | -------------------------------------------- | --------------------------------------------------------------- |
| **OpenAI Agents SDK** | `openai-agents-sdk/` | [View README](./openai-agents-sdk/README.md) | [openai-agents](https://openai.github.io/openai-agents-python/) |
| **Pydantic AI** | `pydantic-ai/` | [View README](./pydantic-ai/README.md) | [pydantic-ai](https://pydantic.dev/docs/ai/overview/) |

## What it can do

Expand Down
16 changes: 16 additions & 0 deletions pydantic-ai/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Required, set at least one AI provider API key.
# If both are set, Anthropic is used by default.
# ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY
OPENAI_API_KEY=YOUR_OPENAI_API_KEY

# Optional, uncomment and set when running without the Slack CLI (python3 app.py).
# SLACK_APP_TOKEN=YOUR_SLACK_APP_TOKEN
# SLACK_BOT_TOKEN=YOUR_SLACK_BOT_TOKEN

# Required for OAuth (app_oauth.py). Set your app's OAuth credentials.
# SLACK_CLIENT_ID=YOUR_SLACK_CLIENT_ID
# SLACK_CLIENT_SECRET=YOUR_SLACK_CLIENT_SECRET
# SLACK_SIGNING_SECRET=YOUR_SLACK_SIGNING_SECRET

# Optional, uncomment and set when using a custom Slack instance.
# SLACK_API_URL=YOUR_SLACK_API_URL
52 changes: 52 additions & 0 deletions pydantic-ai/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Build
build/
dist/
docs/_sources/
docs/.doctrees

# Claude Code
.claude/*.local.json

# Coverage
.coverage
cov_*
coverage.xml

# Editors
.idea/

# Environment
.env*
!.env.sample
env*/
venv/
.venv*

# Logs
logs/

# macOS
.DS_Store

# OAuth
data/

# Python
__pycache__/
*.db
*.egg
*.egg-info/
*.eggs/
*.py[cod]
*.so
.mypy_cache/
.pytype/
.python-version
*~
pip

# Testing
.cache
.pytest_cache/
.tox
tmp.txt
2 changes: 2 additions & 0 deletions pydantic-ai/.slack/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
apps.dev.json
cache/
6 changes: 6 additions & 0 deletions pydantic-ai/.slack/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"manifest": {
"source": "local"
},
"project_id": "3d533454-d0ab-41a8-a99d-e0ac11c33ad9"
}
5 changes: 5 additions & 0 deletions pydantic-ai/.slack/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"hooks": {
"get-hooks": "python3 -m slack_cli_hooks.hooks.get_hooks"
}
}
23 changes: 23 additions & 0 deletions pydantic-ai/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# AGENTS.md

Guidance for coding agents working in this directory. Refer to the root `../AGENTS.md` for repository-wide structure and commands.

## Pydantic AI specifics

**Agents (`agent/agent.py`)** are two Pydantic AI `Agent`s: a routing `request_agent` whose only tool is `create_code_channel`, and the coding `agent` with every other tool. Neither sets a model. A `SelectModel` capability picks one when a run starts, by which API key is set: Anthropic (`claude-haiku-4-5` to route, `claude-sonnet-5-5` to code) when `ANTHROPIC_API_KEY` is set, else OpenAI (`gpt-6-luna` and `gpt-6.1-sol`). So no model client is created at import. Tools are async functions, one per file under `agent/tools/`, passed with `tools=[...]`. Each takes `RunContext[AgentDeps]` (or `RequestDeps`, both in `agent/sessions.py`) for the Slack client and session, and Pydantic AI reads its docstring for the description the model sees. The coding agent lists `AnthropicCompaction` and `OpenAICompaction`, so a long session compacts its oldest conversation instead of outgrowing the context window. Each only applies to its own provider's models.

**Ending a run.** `create_code_channel` and `archive_channel` are output functions (`ToolOutput`), not plain tools, so a successful call ends the run as its output with no further model call. That's Pydantic AI's way to end a run with a tool call. A Slack error in either raises `ModelRetry`, which sends the error back to the model to recover from, such as by replying in place.

**Runs** go through `agent.iter()` from async Bolt listeners. Coding turns allow `MAX_TURNS` model requests with `UsageLimits`. A turn that uses them all ends with a message, and keeps its history by reading the run's messages when `UsageLimitExceeded` is raised.

**Turns** go through `agent/turns.py`, which doesn't import Pydantic AI. A turn that raises posts a warning and moves the session back to `active`, keeping the history from the last finished turn. A tool that raises doesn't fail the turn: the `tool_execute_error` hook logs it and tells the model the tool failed with `ToolFailed`. Model-written replies use `markdown_text`, since the model writes standard markdown.

**Stopping** sets `session.stopped`, and so does archiving the channel from Slack. The `guardrails` capability (`agent/guardrails.py`) skips every tool call once it's set, with `SkipToolExecution`, and tells the model to wrap up. `archive_channel` is an output function, so a hook on the output does the same for it. The turn posts nothing when it finishes.

**Conversation history** is the run's `all_messages()`, a `list[ModelMessage]` stored on the `CodeChannelSession` (`agent/sessions.py`), keyed by code channel. Follow-up turns pass it as `message_history` with the new text as the prompt.

**Command approvals.** `run_command_needs_approval` in `agent/guardrails.py` is a `before_tool_execute` hook for `run_command` that raises `ApprovalRequired` when `command_needs_approval()` from `agent/approvals.py` says so. That ends the run with `DeferredToolRequests` as its output. A paused run is a `PausedRun` (the messages, the requests, and the usage so far), parked in `pending_store` with the ts of its Approve/Deny message, so a click only resumes the run its buttons were posted for. Resuming passes `deferred_tool_results` with `True` or a `ToolDenied`, and the usage, so the turn keeps counting toward `MAX_TURNS`. Every pending call needs an answer, so if one step asked for more than one command, the others are answered as not run and the model can ask again. A newer message in the channel retires a pending approval, so a late click can't resume an older run over it.

## Testing

Tests are async (`pytest-asyncio` in auto mode) and mock the Slack client with `Mock(AsyncWebClient)`. When mocking Pydantic AI objects, use `Mock(spec=...)` with the real class (`AgentRunResult`) so a call to a method that doesn't exist fails the test. `tests/test_runs.py` runs the real agents against `tests/fake_model.py`, a `FunctionModel` that returns scripted tool calls and replies, swapped in with `Agent.override`, so the Pydantic AI seams are tested without calling a provider. `models.ALLOW_MODEL_REQUESTS = False` in `tests/conftest.py` keeps a test from reaching a real model by mistake.
21 changes: 21 additions & 0 deletions pydantic-ai/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Slack Technologies, LLC

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading
Loading