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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ on:
branches:
- "main"

env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_USER_TOKEN: ${{ secrets.SLACK_USER_TOKEN }}
OPEN_ROUTER_TOKEN: ${{ secrets.OPEN_ROUTER_TOKEN }}

jobs:
test:
runs-on: ubuntu-latest
Expand Down
52 changes: 52 additions & 0 deletions app/api/routers/slack_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import os
from pathlib import Path

from dotenv import load_dotenv
from fastapi import APIRouter
from pydantic import BaseModel
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openrouter import OpenRouterProvider
from slack_sdk import WebClient

from app.tools import slack

_ = load_dotenv()

open_router_token = os.environ.get("OPEN_ROUTER_TOKEN", "")
slack_token = os.environ.get("SLACK_USER_TOKEN", "")
client = WebClient(token=slack_token)

prompt_path = Path(__file__).resolve().parents[2] / "pompts" / "slack.md"
prompt_instructions = prompt_path.read_text(encoding="utf-8")

model = OpenAIChatModel(
"z-ai/glm-4.5",
provider=OpenRouterProvider(api_key=open_router_token),
)

agent = Agent(
model=model,
deps_type=slack.Deps,
instructions=prompt_instructions,
)


class Prompt(BaseModel):
user_instruction: str


router = APIRouter()


@router.post("/slack", tags=["slack"])
def prompt_slack_agent(prompt: Prompt):
try:
result = agent.run_sync(
f"{prompt.user_instruction}",
deps=slack.Deps(client=client),
toolsets=[slack.slack_toolset], # type: ignore
)
return {"content": result.output, "status": "ok"}
except Exception as e:
return {"msg": str(e), "status": "failed"}
3 changes: 2 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi import FastAPI

from app.api.routers import health
from app.api.routers import health, slack_agent

app = FastAPI(title="AVA Agents", version="0.1.0")
app.include_router(health.router)
app.include_router(slack_agent.router)
Empty file added app/pompts/__init__.py
Empty file.
50 changes: 50 additions & 0 deletions app/pompts/slack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Role: You are “AVA Slack Operator,” an automation agent that executes Slack tasks for the user

## Core objectives

- Carefully understand the user’s goal, constraints, and urgency.
- Plan the steps before acting; pick the minimal, correct tool(s) to complete the task.
- Execute steps in the right order. Verify results where possible.

## Tool usage

- Use the provided toolsets to perform actions. Do not simulate results.
- When posting a message, use the tool slack.chat.postMessage with parameters:

```text
channel: Slack channel name or ID (e.g., “general”, “#general”, “C123…”). Normalize “#general” to “general” unless an ID is provided.
text: The exact message to send. Preserve formatting, emojis (:rocket:), mentions, code blocks, and links exactly as the user provided unless asked to revise.
```

## Information extraction

Robustly extract the target channel and message text from natural language:

- Examples to recognize: “post ‘hello’ in general”, “send to #general: hello”, “announce in general → hello”, “publish the following in general: …”
- If the user wraps the message in quotes, treat the quoted portion as exact text.
- If multiple channels or none are specified, ask for clarification before posting.
- Avoid adding your own commentary to the message unless explicitly requested.

## Confirmation policy

- If the channel is ambiguous, message content is unclear, or the action seems risky (e.g., mass mentions like @channel/@here), ask for confirmation.
- If the user gives both a clear channel and exact message, proceed without extra confirmation.

## Safety and etiquette

- Do not leak secrets or tokens. Never echo environment values or credentials.
- Respect team etiquette; avoid mass mentions unless explicitly directed.

## Output style

- Internally plan your steps, but don’t expose internal reasoning.
- After tool execution, return a concise confirmation describing what was done and any important result data (e.g., message timestamp).

## Examples

- User: “Post this message ‘Deployment succeeded :tada:’ in channel ‘general’.”
- Plan: Extract channel=“general”, text=“Deployment succeeded :tada:”.
- Tool: slack.chat.postMessage {channel: “general”, text: “Deployment succeeded :tada:”}
- User: “Send to #general: We’ll restart at 5pm.”
- Plan: channel=“general”, text=“We’ll restart at 5pm.”
- Tool: slack.chat.postMessage {channel: “general”, text: “We’ll restart at 5pm.”}
17 changes: 12 additions & 5 deletions app/tools/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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 slack_sdk.errors import SlackApiError

from app.models.tools import SlackChatPostMessageParams

Expand All @@ -17,10 +17,17 @@ class Deps:


@slack_toolset.tool(name="slack.chat.postMessage")
def post_message(
ctx: RunContext[Deps], params: SlackChatPostMessageParams
) -> SlackResponse:
def post_message(ctx: RunContext[Deps], params: SlackChatPostMessageParams) -> dict:
"""
Use this function to post a message in the specified channel
"""
return ctx.deps.client.chat_postMessage(channel=params.channel, text=params.text)
try:
_ = ctx.deps.client.chat_postMessage(channel=params.channel, text=params.text)
return {
"status": 200,
"result": "Message has been posted",
"channel": f"{params.channel}",
"msg": f"{params.text}",
}
except SlackApiError as exc:
return {"status": 500, "Slack API error": f"{exc.response['error']}"}
31 changes: 8 additions & 23 deletions tests/test_ava_slack_post_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,20 @@
_ = 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",
[
(
pytest.param(
"sandbox",
"Hello, AVA backend live smoke :rocket:",
os.environ.get("SLACK_BOT_TOKEN"),
id="bot-token",
),
(
pytest.param(
"general",
"Hello, User is live smoke :rocket:",
os.environ.get("SLACK_USER_TOKEN"),
id="user-token",
),
],
)
Expand All @@ -60,9 +44,10 @@ def test_post_message_with_ctx(channel: str, text: str, token: str | None):
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
assert response["status"] == 200
assert response["result"] == "Message has been posted"
assert response["channel"] == tool_params.channel
assert response["msg"] == tool_params.text


def test_slack_tools_are_synced():
Expand Down
File renamed without changes.
18 changes: 18 additions & 0 deletions tests/test_slack_enpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from fastapi import FastAPI
from fastapi.testclient import TestClient

from app.api.routers import slack_agent

app = FastAPI(title="testing API")
app.include_router(slack_agent.router)


client = TestClient(app)
instructions = (
"Post in the general channel that our team meeting will be delayed by 30 minutes"
)


def test_read_main():
response = client.post("/slack", json={"user_instruction": f"{instructions}"})
assert response.json()["status"] == "ok"
Comment on lines +16 to +18

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Guard Slack endpoint test behind env checks

The new integration test posts to the Slack agent and assumes a live OpenRouter/Slack configuration (response.json()["status"] == "ok"). When the SLACK_USER_TOKEN and OPEN_ROUTER_TOKEN secrets are not available—typical for local contributors—the router returns {..., "status": "failed"} and this assertion will fail, making the entire suite unusable. The other Slack tests already skip when tokens are missing; this one should do the same or mock the agent so tests can be run without external credentials.

Useful? React with 👍 / 👎.