mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-04 16:42:35 +00:00
OpenWorker: initial import
Imported from andrewyng/aisuite@1b4bbf303e (contents of its platform/ directory, hoisted to the repo root). Development history prior to this commit lives in that repository. Co-authored-by: Devika <devikaverma11@gmail.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
"""Stateless outbound senders — one-shot HTTP POSTs, no SDK, no live connection.
|
||||
|
||||
These power the `send_message` tool (and the super-agent's replies). Both Telegram and
|
||||
Slack outbound are simple HTTP calls, so we use a synchronous `httpx` client and avoid the
|
||||
heavy SDKs (those are only needed for the inbound listeners). Sync fits the ToolRegistry's
|
||||
`execute` contract (the engine runs it in a thread).
|
||||
|
||||
A `Sender` is `(token, chat_id, text, thread_id) -> SendResult`. The registry is swappable so
|
||||
tests inject fakes — no network.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Callable, Optional
|
||||
|
||||
from .base import SendResult
|
||||
|
||||
Sender = Callable[[str, str, str, Optional[str]], SendResult]
|
||||
|
||||
_TIMEOUT = 30.0
|
||||
|
||||
|
||||
def _slack_api_base() -> str:
|
||||
"""Web API base URL. `SLACK_API_URL` (trailing slash) lets tests / the FakeSlack harness
|
||||
redirect outbound sends to a local fake. See platform/docs/FAKE-SLACK-SPEC.md."""
|
||||
return os.environ.get("SLACK_API_URL", "https://slack.com/api/")
|
||||
|
||||
|
||||
def _send_telegram(
|
||||
token: str, chat_id: str, text: str, thread_id: Optional[str] = None
|
||||
) -> SendResult:
|
||||
import httpx
|
||||
|
||||
payload: dict = {"chat_id": chat_id, "text": text}
|
||||
# Telegram's General forum topic is thread_id "1", which sendMessage rejects → omit it.
|
||||
if thread_id and thread_id != "1":
|
||||
try:
|
||||
payload["message_thread_id"] = int(thread_id)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
resp = httpx.post(
|
||||
f"https://api.telegram.org/bot{token}/sendMessage",
|
||||
json=payload,
|
||||
timeout=_TIMEOUT,
|
||||
)
|
||||
data = resp.json()
|
||||
except Exception as exc: # network / decode
|
||||
return SendResult(False, error=str(exc))
|
||||
if data.get("ok"):
|
||||
return SendResult(
|
||||
True, message_id=str(data.get("result", {}).get("message_id"))
|
||||
)
|
||||
return SendResult(False, error=data.get("description") or "telegram send failed")
|
||||
|
||||
|
||||
def _send_slack(
|
||||
token: str, chat_id: str, text: str, thread_id: Optional[str] = None
|
||||
) -> SendResult:
|
||||
import httpx
|
||||
|
||||
from .slack_addr import split
|
||||
|
||||
# A managed-relay chat_id is team-qualified ("T…/C…"); Slack's API wants the
|
||||
# bare channel. The per-team token is selected by the caller (send_message).
|
||||
_team, chat_id = split(chat_id)
|
||||
payload: dict = {"channel": chat_id, "text": text}
|
||||
if thread_id:
|
||||
payload["thread_ts"] = thread_id
|
||||
try:
|
||||
resp = httpx.post(
|
||||
f"{_slack_api_base()}chat.postMessage",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json=payload,
|
||||
timeout=_TIMEOUT,
|
||||
)
|
||||
data = resp.json()
|
||||
except Exception as exc:
|
||||
return SendResult(False, error=str(exc))
|
||||
if data.get("ok"):
|
||||
return SendResult(True, message_id=data.get("ts"))
|
||||
err = data.get("error") or "slack send failed"
|
||||
if err == "not_in_channel":
|
||||
err = "not_in_channel — invite @ocw to the channel in Slack, then retry"
|
||||
return SendResult(False, error=err)
|
||||
|
||||
|
||||
def _slack_blocks(text: str, buttons) -> list[dict]:
|
||||
"""A Block Kit message: a text section + a row of action buttons (action_id `ocw_<i>`,
|
||||
value = the encoded item id + resolution)."""
|
||||
blocks: list[dict] = [{"type": "section", "text": {"type": "mrkdwn", "text": text}}]
|
||||
if buttons:
|
||||
blocks.append(
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": b.label[:75]},
|
||||
"value": b.value,
|
||||
"action_id": f"ocw_{i}",
|
||||
}
|
||||
for i, b in enumerate(buttons)
|
||||
],
|
||||
}
|
||||
)
|
||||
return blocks
|
||||
|
||||
|
||||
def _send_slack_interactive(
|
||||
token: str, chat_id: str, text: str, buttons, thread_id: Optional[str] = None
|
||||
) -> SendResult:
|
||||
import httpx
|
||||
|
||||
from .slack_addr import split
|
||||
|
||||
_team, chat_id = split(chat_id)
|
||||
payload: dict = {
|
||||
"channel": chat_id,
|
||||
"text": text,
|
||||
"blocks": _slack_blocks(text, buttons),
|
||||
}
|
||||
if thread_id:
|
||||
payload["thread_ts"] = thread_id
|
||||
try:
|
||||
resp = httpx.post(
|
||||
f"{_slack_api_base()}chat.postMessage",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json=payload,
|
||||
timeout=_TIMEOUT,
|
||||
)
|
||||
data = resp.json()
|
||||
except Exception as exc:
|
||||
return SendResult(False, error=str(exc))
|
||||
if data.get("ok"):
|
||||
return SendResult(True, message_id=data.get("ts"))
|
||||
return SendResult(False, error=data.get("error") or "slack send failed")
|
||||
|
||||
|
||||
DEFAULT_SENDERS: dict[str, Sender] = {
|
||||
"telegram": _send_telegram,
|
||||
"slack": _send_slack,
|
||||
}
|
||||
|
||||
|
||||
# -- file upload (§34 / UX-016) --------------------------------------------------------
|
||||
# A FileSender is (token, chat_id, thread_id, filename, data, title, comment) -> SendResult.
|
||||
FileSender = Callable[
|
||||
[str, str, Optional[str], str, bytes, Optional[str], Optional[str]], SendResult
|
||||
]
|
||||
|
||||
|
||||
def _send_slack_file(
|
||||
token: str,
|
||||
chat_id: str,
|
||||
thread_id: Optional[str],
|
||||
filename: str,
|
||||
data: bytes,
|
||||
title: Optional[str] = None,
|
||||
comment: Optional[str] = None,
|
||||
) -> SendResult:
|
||||
"""files_upload_v2 (the only non-deprecated path): reserve an upload URL, PUT the
|
||||
bytes, then complete into the channel/thread. Slack renders its own previews for
|
||||
pdf/csv/images — that's the whole point of sending the file instead of a thumbnail.
|
||||
"""
|
||||
import httpx
|
||||
|
||||
from .slack_addr import split
|
||||
|
||||
_team, chat_id = split(chat_id)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
try:
|
||||
resp = httpx.post(
|
||||
f"{_slack_api_base()}files.getUploadURLExternal",
|
||||
headers=headers,
|
||||
data={"filename": filename, "length": str(len(data))},
|
||||
timeout=_TIMEOUT,
|
||||
)
|
||||
got = resp.json()
|
||||
if not got.get("ok"):
|
||||
return SendResult(
|
||||
False, error=got.get("error") or "slack upload-url failed"
|
||||
)
|
||||
up = httpx.post(
|
||||
got["upload_url"],
|
||||
files={"file": (filename, data)},
|
||||
timeout=max(_TIMEOUT, 120.0),
|
||||
)
|
||||
if up.status_code != 200:
|
||||
return SendResult(False, error=f"slack upload failed ({up.status_code})")
|
||||
complete: dict = {
|
||||
"files": [{"id": got["file_id"], "title": title or filename}],
|
||||
"channel_id": chat_id,
|
||||
}
|
||||
if thread_id:
|
||||
complete["thread_ts"] = thread_id
|
||||
if comment:
|
||||
complete["initial_comment"] = comment
|
||||
resp = httpx.post(
|
||||
f"{_slack_api_base()}files.completeUploadExternal",
|
||||
headers=headers,
|
||||
json=complete,
|
||||
timeout=_TIMEOUT,
|
||||
)
|
||||
data_out = resp.json()
|
||||
except Exception as exc: # network / decode
|
||||
return SendResult(False, error=str(exc))
|
||||
if data_out.get("ok"):
|
||||
return SendResult(True, message_id=got["file_id"])
|
||||
return SendResult(False, error=data_out.get("error") or "slack file send failed")
|
||||
|
||||
|
||||
DEFAULT_FILE_SENDERS: dict[str, FileSender] = {
|
||||
"slack": _send_slack_file,
|
||||
}
|
||||
Reference in New Issue
Block a user