Start Slack toolkit (#17)
- Start a Slack toolkit with a few tools - Update Google auth - Show user's email in `arcade chat`
This commit is contained in:
parent
3154298572
commit
acba912816
14 changed files with 272 additions and 57 deletions
|
|
@ -220,17 +220,30 @@ def chat(
|
||||||
)
|
)
|
||||||
console.print(chat_header)
|
console.print(chat_header)
|
||||||
|
|
||||||
|
user = config.user.email if config.user and config.user.email else None
|
||||||
|
user_attribution = f" ({user})" if user else ""
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
user_input = console.input("\n[bold magenta]User: [/bold magenta]")
|
user_input = console.input(
|
||||||
|
f"\n[magenta][bold]User[/bold]{user_attribution}:[/magenta] "
|
||||||
|
)
|
||||||
messages.append({"role": "user", "content": user_input})
|
messages.append({"role": "user", "content": user_input})
|
||||||
|
|
||||||
if stream:
|
if stream:
|
||||||
stream_response = client.stream_complete(
|
stream_response = client.stream_complete(
|
||||||
model=model, messages=messages, tool_choice="generate"
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
tool_choice="generate",
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
display_streamed_markdown(stream_response)
|
display_streamed_markdown(stream_response)
|
||||||
else:
|
else:
|
||||||
response = client.complete(model=model, messages=messages, tool_choice="generate")
|
response = client.complete(
|
||||||
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
tool_choice="generate",
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
message_content = response.choices[0].message.content or ""
|
message_content = response.choices[0].message.content or ""
|
||||||
role = response.choices[0].message.role
|
role = response.choices[0].message.role
|
||||||
|
|
||||||
|
|
@ -350,7 +363,7 @@ def display_config_as_table(config: Config) -> None:
|
||||||
table.add_column("Name")
|
table.add_column("Name")
|
||||||
table.add_column("Value")
|
table.add_column("Value")
|
||||||
|
|
||||||
for section_name in config.dict():
|
for section_name in config.model_dump():
|
||||||
section = getattr(config, section_name)
|
section = getattr(config, section_name)
|
||||||
if section:
|
if section:
|
||||||
section = section.dict()
|
section = section.dict()
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ from arcade.core.schema import (
|
||||||
GoogleRequirement,
|
GoogleRequirement,
|
||||||
InputParameter,
|
InputParameter,
|
||||||
OAuth2Requirement,
|
OAuth2Requirement,
|
||||||
|
SlackUserRequirement,
|
||||||
ToolAuthRequirement,
|
ToolAuthRequirement,
|
||||||
ToolContext,
|
ToolContext,
|
||||||
ToolDefinition,
|
ToolDefinition,
|
||||||
|
|
@ -42,7 +43,7 @@ from arcade.core.utils import (
|
||||||
snake_to_pascal_case,
|
snake_to_pascal_case,
|
||||||
)
|
)
|
||||||
from arcade.sdk.annotations import Inferrable
|
from arcade.sdk.annotations import Inferrable
|
||||||
from arcade.sdk.auth import Google, OAuth2, ToolAuthorization
|
from arcade.sdk.auth import Google, OAuth2, SlackUser, ToolAuthorization
|
||||||
|
|
||||||
WireType = Literal["string", "integer", "float", "boolean", "json"]
|
WireType = Literal["string", "integer", "float", "boolean", "json"]
|
||||||
|
|
||||||
|
|
@ -190,6 +191,10 @@ class ToolCatalog(BaseModel):
|
||||||
new_auth_requirement.oauth2 = OAuth2Requirement(**auth_requirement.model_dump())
|
new_auth_requirement.oauth2 = OAuth2Requirement(**auth_requirement.model_dump())
|
||||||
elif isinstance(auth_requirement, Google):
|
elif isinstance(auth_requirement, Google):
|
||||||
new_auth_requirement.google = GoogleRequirement(**auth_requirement.model_dump())
|
new_auth_requirement.google = GoogleRequirement(**auth_requirement.model_dump())
|
||||||
|
elif isinstance(auth_requirement, SlackUser):
|
||||||
|
new_auth_requirement.slack_user = SlackUserRequirement(
|
||||||
|
**auth_requirement.model_dump()
|
||||||
|
)
|
||||||
auth_requirement = new_auth_requirement
|
auth_requirement = new_auth_requirement
|
||||||
|
|
||||||
return ToolDefinition(
|
return ToolDefinition(
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,13 @@ class GoogleRequirement(BaseModel):
|
||||||
"""The scope(s) needed for authorization."""
|
"""The scope(s) needed for authorization."""
|
||||||
|
|
||||||
|
|
||||||
|
class SlackUserRequirement(BaseModel):
|
||||||
|
"""Indicates that the tool requires Slack (user token) authorization."""
|
||||||
|
|
||||||
|
scope: Optional[list[str]] = None
|
||||||
|
"""The scope(s) needed for authorization."""
|
||||||
|
|
||||||
|
|
||||||
class ToolAuthRequirement(BaseModel):
|
class ToolAuthRequirement(BaseModel):
|
||||||
"""A requirement for authorization to use a tool."""
|
"""A requirement for authorization to use a tool."""
|
||||||
|
|
||||||
|
|
@ -91,6 +98,9 @@ class ToolAuthRequirement(BaseModel):
|
||||||
google: Optional[GoogleRequirement] = None
|
google: Optional[GoogleRequirement] = None
|
||||||
"""The Google requirement, if any."""
|
"""The Google requirement, if any."""
|
||||||
|
|
||||||
|
slack_user: Optional[SlackUserRequirement] = None
|
||||||
|
"""The Slack (user token) requirement, if any."""
|
||||||
|
|
||||||
|
|
||||||
class ToolRequirements(BaseModel):
|
class ToolRequirements(BaseModel):
|
||||||
"""The requirements for a tool to run."""
|
"""The requirements for a tool to run."""
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,16 @@ class Google(ToolAuthorization):
|
||||||
"""The scope(s) needed for the authorized action."""
|
"""The scope(s) needed for the authorized action."""
|
||||||
|
|
||||||
|
|
||||||
|
class SlackUser(ToolAuthorization):
|
||||||
|
"""Marks a tool as requiring Slack (user token) authorization."""
|
||||||
|
|
||||||
|
def get_provider(self) -> str:
|
||||||
|
return "slack_user"
|
||||||
|
|
||||||
|
scope: Optional[list[str]] = None
|
||||||
|
"""The scope(s) needed for the authorized action."""
|
||||||
|
|
||||||
|
|
||||||
class GitHubApp(ToolAuthorization):
|
class GitHubApp(ToolAuthorization):
|
||||||
"""Marks a tool as requiring GitHub App authorization."""
|
"""Marks a tool as requiring GitHub App authorization."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ from fastapi import FastAPI, HTTPException
|
||||||
from openai import AsyncOpenAI
|
from openai import AsyncOpenAI
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from arcade_arithmetic.tools import arithmetic
|
|
||||||
from arcade_gmail.tools import gmail
|
from arcade_gmail.tools import gmail
|
||||||
from arcade_github.tools import public_repo, user
|
from arcade_github.tools import repo, user
|
||||||
|
from arcade_slack.tools import chat
|
||||||
|
|
||||||
from arcade.actor.fastapi.actor import FastAPIActor
|
from arcade.actor.fastapi.actor import FastAPIActor
|
||||||
|
|
||||||
|
|
@ -13,13 +13,16 @@ client = AsyncOpenAI(base_url="http://localhost:9099/v1")
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
actor = FastAPIActor(app)
|
actor = FastAPIActor(app)
|
||||||
actor.register_tool(arithmetic.add)
|
# actor.register_tool(arithmetic.add)
|
||||||
actor.register_tool(arithmetic.multiply)
|
# actor.register_tool(arithmetic.multiply)
|
||||||
actor.register_tool(arithmetic.divide)
|
# actor.register_tool(arithmetic.divide)
|
||||||
actor.register_tool(arithmetic.sqrt)
|
# actor.register_tool(arithmetic.sqrt)
|
||||||
actor.register_tool(gmail.get_emails)
|
actor.register_tool(gmail.get_emails)
|
||||||
actor.register_tool(public_repo.count_stargazers)
|
actor.register_tool(gmail.write_draft)
|
||||||
|
actor.register_tool(repo.count_stargazers)
|
||||||
|
actor.register_tool(repo.search_issues)
|
||||||
actor.register_tool(user.set_starred)
|
actor.register_tool(user.set_starred)
|
||||||
|
actor.register_tool(chat.send_dm_to_user)
|
||||||
|
|
||||||
|
|
||||||
class ChatRequest(BaseModel):
|
class ChatRequest(BaseModel):
|
||||||
|
|
@ -27,7 +30,7 @@ class ChatRequest(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
@app.post("/chat")
|
@app.post("/chat")
|
||||||
async def chat(request: ChatRequest, tool_choice: str = "execute"):
|
async def postChat(request: ChatRequest, tool_choice: str = "execute"):
|
||||||
try:
|
try:
|
||||||
raw_response = await client.chat.completions.create(
|
raw_response = await client.chat.completions.create(
|
||||||
messages=[
|
messages=[
|
||||||
|
|
@ -35,16 +38,15 @@ async def chat(request: ChatRequest, tool_choice: str = "execute"):
|
||||||
{"role": "user", "content": request.message},
|
{"role": "user", "content": request.message},
|
||||||
],
|
],
|
||||||
model="gpt-4o-mini",
|
model="gpt-4o-mini",
|
||||||
max_tokens=150,
|
max_tokens=500,
|
||||||
# TODO tests for tool choice
|
# TODO tests for tool choice
|
||||||
tools=[
|
tools=[
|
||||||
"Add",
|
|
||||||
"Multiply",
|
|
||||||
"Divide",
|
|
||||||
"Sqrt",
|
|
||||||
"GetEmails",
|
"GetEmails",
|
||||||
|
"WriteDraft",
|
||||||
"CountStargazers",
|
"CountStargazers",
|
||||||
"SetStarred",
|
"SetStarred",
|
||||||
|
"SearchIssues",
|
||||||
|
"SendDmToUser",
|
||||||
],
|
],
|
||||||
tool_choice=tool_choice,
|
tool_choice=tool_choice,
|
||||||
user="sam",
|
user="sam",
|
||||||
|
|
|
||||||
45
examples/fastapi/poetry.lock
generated
45
examples/fastapi/poetry.lock
generated
|
|
@ -116,15 +116,32 @@ googleapis-common-protos = "1.63.2"
|
||||||
type = "directory"
|
type = "directory"
|
||||||
url = "../../toolkits/gmail"
|
url = "../../toolkits/gmail"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arcade-slack"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Slack tools for LLMs"
|
||||||
|
optional = false
|
||||||
|
python-versions = "^3.10"
|
||||||
|
files = []
|
||||||
|
develop = true
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
arcade-ai = "^0.1.0"
|
||||||
|
slack-sdk = "^3.31.0"
|
||||||
|
|
||||||
|
[package.source]
|
||||||
|
type = "directory"
|
||||||
|
url = "../../toolkits/slack"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cachetools"
|
name = "cachetools"
|
||||||
version = "5.4.0"
|
version = "5.5.0"
|
||||||
description = "Extensible memoizing collections and decorators"
|
description = "Extensible memoizing collections and decorators"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"},
|
{file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"},
|
||||||
{file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"},
|
{file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -625,13 +642,13 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "1.40.8"
|
version = "1.42.0"
|
||||||
description = "The official Python library for the openai API"
|
description = "The official Python library for the openai API"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7.1"
|
python-versions = ">=3.7.1"
|
||||||
files = [
|
files = [
|
||||||
{file = "openai-1.40.8-py3-none-any.whl", hash = "sha256:3ed4ddad48e0dde059c9b4d3dc240e47781beca2811e52ba449ddc4a471a2fd4"},
|
{file = "openai-1.42.0-py3-none-any.whl", hash = "sha256:dc91e0307033a4f94931e5d03cc3b29b9717014ad5e73f9f2051b6cb5eda4d80"},
|
||||||
{file = "openai-1.40.8.tar.gz", hash = "sha256:e225f830b946378e214c5b2cfa8df28ba2aeb7e9d44f738cb2a926fd971f5bc0"},
|
{file = "openai-1.42.0.tar.gz", hash = "sha256:c9d31853b4e0bc2dc8bd08003b462a006035655a701471695d0bfdc08529cde3"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -982,6 +999,20 @@ files = [
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pyasn1 = ">=0.1.3"
|
pyasn1 = ">=0.1.3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slack-sdk"
|
||||||
|
version = "3.31.0"
|
||||||
|
description = "The Slack API Platform SDK for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "slack_sdk-3.31.0-py2.py3-none-any.whl", hash = "sha256:a120cc461e8ebb7d9175f171dbe0ded37a6878d9f7b96b28e4bad1227399047b"},
|
||||||
|
{file = "slack_sdk-3.31.0.tar.gz", hash = "sha256:740d2f9c49cbfcbd46fca56b4be9d527934c225312aac18fd2c0fca0ef6bc935"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=9.1,<13)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sniffio"
|
name = "sniffio"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
|
|
@ -1115,4 +1146,4 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "ba5d3be15ef9c2adf5c108b4f8ff3388f3a4df546fa84f0bcfeb56c2768171ad"
|
content-hash = "b40d9fea27174b3ac18d83a936744819ed484e1baa2aaba5ab54ae9e4681c416"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ arcade-ai = {path = "../../arcade", develop = true}
|
||||||
arcade_arithmetic = {path = "../../toolkits/math", develop = true}
|
arcade_arithmetic = {path = "../../toolkits/math", develop = true}
|
||||||
arcade_gmail = {path = "../../toolkits/gmail", develop = true}
|
arcade_gmail = {path = "../../toolkits/gmail", develop = true}
|
||||||
arcade_github = {path = "../../toolkits/github", develop = true}
|
arcade_github = {path = "../../toolkits/github", develop = true}
|
||||||
|
arcade_slack = {path = "../../toolkits/slack", develop = true}
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
from typing import Annotated
|
|
||||||
from arcade.sdk import tool
|
|
||||||
import requests
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: This does not support private repositories. https://app.clickup.com/t/86b1r3mhe
|
|
||||||
@tool
|
|
||||||
def count_stargazers(
|
|
||||||
owner: Annotated[str, "The owner of the repository"],
|
|
||||||
name: Annotated[str, "The name of the repository"],
|
|
||||||
) -> int:
|
|
||||||
"""Count the number of stargazers (stars) for a public GitHub repository.
|
|
||||||
|
|
||||||
For example, to count the number of stars for microsoft/vscode, you would use:
|
|
||||||
```
|
|
||||||
count_stargazers(owner="microsoft", name="vscode")
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
|
|
||||||
url = f"https://api.github.com/repos/{owner}/{name}"
|
|
||||||
response = requests.get(url)
|
|
||||||
|
|
||||||
print(response)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
return data.get("stargazers_count", 0)
|
|
||||||
else:
|
|
||||||
raise Exception(
|
|
||||||
f"Failed to fetch repository data. Status code: {response.status_code}"
|
|
||||||
)
|
|
||||||
74
toolkits/github/arcade_github/tools/repo.py
Normal file
74
toolkits/github/arcade_github/tools/repo.py
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
from typing import Annotated
|
||||||
|
from arcade.core.schema import ToolContext
|
||||||
|
from arcade.sdk import tool
|
||||||
|
from arcade.sdk.auth import GitHubApp
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
@tool(requires_auth=GitHubApp())
|
||||||
|
def search_issues(
|
||||||
|
context: ToolContext,
|
||||||
|
owner: Annotated[str, "The owner of the repository"],
|
||||||
|
name: Annotated[str, "The name of the repository"],
|
||||||
|
query: Annotated[str, "The query to search for"],
|
||||||
|
limit: Annotated[int, "The maximum number of issues to return"] = 10,
|
||||||
|
) -> dict[str, list[dict]]:
|
||||||
|
"""Search for issues in a GitHub repository."""
|
||||||
|
|
||||||
|
# Build the search query
|
||||||
|
url = f"https://api.github.com/search/issues?q={query}+is:issue+is:open+repo:{owner}/{name}+sort:created-desc&per_page={limit}"
|
||||||
|
|
||||||
|
# Make the API request
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"token {context.authorization.token}",
|
||||||
|
"Accept": "application/vnd.github.v3+json",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
|
||||||
|
# Check for successful response
|
||||||
|
# handle 422 for can't find repo
|
||||||
|
# TODO - how should errors bubble back up if tool_choice=execute
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise Exception(f"Failed to fetch issues: {response.status_code}")
|
||||||
|
|
||||||
|
issues = response.json().get("items", [])
|
||||||
|
results = []
|
||||||
|
for issue in issues:
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"title": issue["title"],
|
||||||
|
"url": issue["html_url"],
|
||||||
|
"created_at": issue["created_at"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"issues": results}
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: This does not support private repositories. https://app.clickup.com/t/86b1r3mhe
|
||||||
|
@tool
|
||||||
|
def count_stargazers(
|
||||||
|
owner: Annotated[str, "The owner of the repository"],
|
||||||
|
name: Annotated[str, "The name of the repository"],
|
||||||
|
) -> int:
|
||||||
|
"""Count the number of stargazers (stars) for a public GitHub repository.
|
||||||
|
|
||||||
|
For example, to count the number of stars for microsoft/vscode, you would use:
|
||||||
|
```
|
||||||
|
count_stargazers(owner="microsoft", name="vscode")
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = f"https://api.github.com/repos/{owner}/{name}"
|
||||||
|
response = requests.get(url)
|
||||||
|
|
||||||
|
print(response)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
return data.get("stargazers_count", 0)
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
f"Failed to fetch repository data. Status code: {response.status_code}"
|
||||||
|
)
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
import base64
|
||||||
|
from email.mime.text import MIMEText
|
||||||
import re
|
import re
|
||||||
from base64 import urlsafe_b64decode
|
from base64 import urlsafe_b64decode
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from google.auth.credentials import Credentials
|
from google.oauth2.credentials import Credentials
|
||||||
from googleapiclient.discovery import build
|
from googleapiclient.discovery import build
|
||||||
|
|
||||||
from arcade.core.schema import ToolContext
|
from arcade.core.schema import ToolContext
|
||||||
|
|
@ -11,6 +13,40 @@ from arcade.sdk import tool
|
||||||
from arcade.sdk.auth import Google
|
from arcade.sdk.auth import Google
|
||||||
|
|
||||||
|
|
||||||
|
@tool(
|
||||||
|
requires_auth=Google(
|
||||||
|
scope=["https://www.googleapis.com/auth/gmail.compose"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
async def write_draft(
|
||||||
|
context: ToolContext,
|
||||||
|
subject: Annotated[str, "The subject of the email"],
|
||||||
|
body: Annotated[str, "The body of the email"],
|
||||||
|
recipient: Annotated[str, "The recipient of the email"],
|
||||||
|
) -> Annotated[str, "The URL of the draft"]:
|
||||||
|
"""Compose a new email draft."""
|
||||||
|
|
||||||
|
# Set up the Gmail API client
|
||||||
|
service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
|
||||||
|
|
||||||
|
message = MIMEText(body)
|
||||||
|
message["to"] = recipient
|
||||||
|
message["subject"] = subject
|
||||||
|
|
||||||
|
# Encode the message in base64
|
||||||
|
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
|
||||||
|
|
||||||
|
# Create the draft
|
||||||
|
draft = {"message": {"raw": raw_message}}
|
||||||
|
|
||||||
|
draft_message = service.users().drafts().create(userId="me", body=draft).execute()
|
||||||
|
return f"Draft created: {get_draft_url(draft_message['id'])}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_draft_url(draft_id):
|
||||||
|
return f"https://mail.google.com/mail/u/0/#drafts/{draft_id}"
|
||||||
|
|
||||||
|
|
||||||
@tool(
|
@tool(
|
||||||
requires_auth=Google(
|
requires_auth=Google(
|
||||||
scope=["https://www.googleapis.com/auth/gmail.readonly"],
|
scope=["https://www.googleapis.com/auth/gmail.readonly"],
|
||||||
|
|
|
||||||
0
toolkits/slack/arcade_slack/__init__.py
Normal file
0
toolkits/slack/arcade_slack/__init__.py
Normal file
0
toolkits/slack/arcade_slack/tools/__init__.py
Normal file
0
toolkits/slack/arcade_slack/tools/__init__.py
Normal file
47
toolkits/slack/arcade_slack/tools/chat.py
Normal file
47
toolkits/slack/arcade_slack/tools/chat.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
from typing import Annotated
|
||||||
|
from arcade.core.schema import ToolContext
|
||||||
|
from arcade.sdk import tool
|
||||||
|
from arcade.sdk.auth import SlackUser
|
||||||
|
from slack_sdk import WebClient
|
||||||
|
from slack_sdk.errors import SlackApiError
|
||||||
|
|
||||||
|
|
||||||
|
@tool(
|
||||||
|
requires_auth=SlackUser(
|
||||||
|
scope=["chat:write", "im:write", "users.profile:read", "users:read"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
def send_dm_to_user(
|
||||||
|
context: ToolContext,
|
||||||
|
user_name: Annotated[str, "The Slack username of the person you want to message"],
|
||||||
|
message: Annotated[str, "The message you want to send"],
|
||||||
|
):
|
||||||
|
"""Send a direct message to a user in Slack."""
|
||||||
|
|
||||||
|
slackClient = WebClient(token=context.authorization.token)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Step 1: Retrieve the user's Slack ID based on their username
|
||||||
|
response = slackClient.users_list()
|
||||||
|
user_id = None
|
||||||
|
for user in response["members"]:
|
||||||
|
if user["name"].lower() == user_name.lower():
|
||||||
|
user_id = user["id"]
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
# does this end up as a developerMessage?
|
||||||
|
# does it end up in the LLM context?
|
||||||
|
# provide the dev an Error type that controls what ends up in the LLM context
|
||||||
|
raise ValueError(f"User with username '{user_name}' not found.")
|
||||||
|
|
||||||
|
# Step 2: Retrieve the DM channel ID with the user
|
||||||
|
im_response = slackClient.conversations_open(users=[user_id])
|
||||||
|
dm_channel_id = im_response["channel"]["id"]
|
||||||
|
|
||||||
|
# Step 3: Send the message as if it's from you (because we're using a user token)
|
||||||
|
slackClient.chat_postMessage(channel=dm_channel_id, text=message)
|
||||||
|
|
||||||
|
except SlackApiError as e:
|
||||||
|
# this should be caught also, not printed
|
||||||
|
print(f"Error sending message: {e.response['error']}")
|
||||||
17
toolkits/slack/pyproject.toml
Normal file
17
toolkits/slack/pyproject.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "arcade_slack"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Slack tools for LLMs"
|
||||||
|
authors = ["Nate Barbettini <nate@arcade-ai.com>"]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.10"
|
||||||
|
arcade-ai = "^0.1.0"
|
||||||
|
slack-sdk = "^3.31.0"
|
||||||
|
|
||||||
|
[tool.poetry.dev-dependencies]
|
||||||
|
pytest = "^7.4.0"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
Loading…
Reference in a new issue