diff --git a/.env.example b/.env.example index 75cd5ca..cc05a97 100644 --- a/.env.example +++ b/.env.example @@ -38,6 +38,18 @@ SLACK_TEAM_ID= SLACK_USER_IDS= # SLACK_DOT_ID= (defaults to the initial Dot) +# Optional direct Telegram channel. Long-polling is the default and does not +# require a public URL. Restrict access with explicit Telegram numeric user IDs. +TELEGRAM_BOT_TOKEN= +# TELEGRAM_CHANNEL_NAME=opendots-telegram +TELEGRAM_USER_IDS= +# TELEGRAM_DOT_ID= (defaults to the initial Dot) +# TELEGRAM_MODE=polling +# TELEGRAM_WEBHOOK_DOMAIN=https://bot.example.com +# TELEGRAM_WEBHOOK_PATH=/telegram +# TELEGRAM_WEBHOOK_PORT=8443 +# TELEGRAM_WEBHOOK_SECRET= + # Optional persistent computers, one per Dot, using OpenBot services. # See docs/COMPUTERS.md. Keep these two different random secrets on the server. COMPUTER_SUPERVISOR_URL= diff --git a/README.md b/README.md index b1ea8f2..2ce6797 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # OpenDots -### Always-on AI coworkers that move between text, calls, and Slack. +### Always-on AI coworkers that move between text, calls, Slack, and Telegram. **An open-source template for persistent AI agents, each with its own computer. Available on Web and Mobile.** @@ -100,6 +100,17 @@ https://github.com/user-attachments/assets/3c06cf71-39ed-4e2b-b846-5463b2722389 _Connect, talk, mute, minimize, and return to chat. This is a silent screen capture of a real call, with waiting time trimmed and playback accelerated._ +### Telegram + +Connect a Telegram bot directly to OpenDots with the CopilotKit Channels Telegram adapter. Long-polling is the default, so a public webhook endpoint is not required. Telegram users are mapped to the single OpenDots owner through an explicit numeric user-ID allowlist. + +In a private chat, every message is eligible. In groups, the bot responds when it is mentioned or when a user replies to one of its messages. The same Dot, tools, permissions, memory, and Intelligence conversation flow are used as web and Slack. + +See [Telegram setup](docs/SETUP.md#telegram) to configure the bot token, allowlist, selected Dot, and optional webhook mode. +### Inbox and Watchers + +OpenDots includes a small proactive layer on top of scheduled work. The Inbox collects completed and failed task outcomes and watcher triggers in one place. Watchers monitor public HTTP(S) URLs and queue a normal task in an existing conversation when content changes. + ### Slack Mention a Dot through a managed Slack connection using Channels SDK, then continue in its thread. The integration follows [OpenTag](https://github.com/CopilotKit/OpenTag), with an explicit workspace/user allowlist and a selected specialist. See [Slack setup](docs/SETUP.md#slack) to connect your deployment. @@ -128,7 +139,8 @@ The template uses TanStack AI for model streaming and server-tool execution, Cop flowchart TB Web["Web app: pages, Spaces, Dots, chat"] -->|AG-UI| Runtime[CopilotKit runtime] Slack[Slack] <--> Managed[Managed channel connection] - Managed <--> Channels[Channels SDK] + Telegram[Telegram] --> Channels[Channels SDK] + Managed <--> Channels Channels --> Agents[Specialist compute agents] Runtime --> Agents Agents --> AI[TanStack AI] @@ -172,6 +184,7 @@ See [Setup](docs/SETUP.md) for configuration, Slack, calls, the browser service, | Pages | Searchable library, visual editor, slash commands, autosave, and revision checks | | Conversations | React SDK chat and Threads integration, page-specific conversations, and source links | | Slack | Managed Channels SDK declaration with workspace and user allowlists | +| Telegram | Direct Channels SDK adapter with explicit user allowlist and polling/webhook ingress | | Calls | WebRTC speech, delegated compute, bounded sessions, hangup, and timeline receipts | | Background work | Scheduled server-side turns in their original conversation, with pause and retry controls | | Browser | Separate read-only public-page service with page capture and navigation limits | diff --git a/docs/SETUP.md b/docs/SETUP.md index 5d883ce..b9620be 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -107,6 +107,58 @@ From an allowed user, mention the bot and verify a response in the same Slack th Local tests exercise channel behavior with fixtures. A live Slack mention/reply remains unverified until you provision the managed connection and model credentials. [Channels SDK documentation](https://github.com/CopilotKit/channels-sdk) describes extending the adapter and channel behavior. +## Telegram + +OpenDots can run a direct Telegram bot through the CopilotKit Channels Telegram adapter. This does not require creating a managed Intelligence Channel; the adapter is attached to the same runtime as the application's other conversations. + +### Configure the bot + +Create a bot with Telegram's `@BotFather`, then set these server-side variables: + +```dotenv +TELEGRAM_BOT_TOKEN=123456:replace-me +TELEGRAM_CHANNEL_NAME=opendots-telegram +TELEGRAM_USER_IDS=123456789 +TELEGRAM_DOT_ID= +TELEGRAM_MODE=polling +``` + +Use a comma-separated list for more than one permitted Telegram user. OpenDots requires an explicit allowlist because the template has a single-owner identity model. The Telegram bot token and IDs stay on the server. + +`TELEGRAM_DOT_ID` selects the Dot used for Telegram turns; when omitted, the first Dot is used. `TELEGRAM_CHANNEL_NAME` names the runtime Channel and defaults to `opendots-telegram` when a bot token is configured. + +### Polling + +Long-polling is the default: + +```dotenv +TELEGRAM_MODE=polling +``` + +No public URL is required. Start OpenDots normally and verify Telegram status under Settings & setup. + +In private chats, every user message is addressed to the bot. In groups and supergroups, the adapter only emits turns when the bot is mentioned or the user replies to one of the bot's messages. Forum topics keep their own Telegram conversation context. + +### Webhook + +For deployments where long-polling is not suitable, use webhook mode: + +```dotenv +TELEGRAM_MODE=webhook +TELEGRAM_WEBHOOK_DOMAIN=https://bot.example.com +TELEGRAM_WEBHOOK_PATH=/telegram +TELEGRAM_WEBHOOK_PORT=8443 +TELEGRAM_WEBHOOK_SECRET=replace-with-a-random-secret +``` + +The domain must be publicly reachable over HTTPS and point to the OpenDots process. Telegram's supported webhook ports include 443, 80, 88, and 8443; the adapter defaults to 8443. Put the webhook endpoint behind your reverse proxy when that is how the application is exposed. + +### Verify + +Send `/start` to the bot, then send a normal message and verify the selected Dot answers. In a group, mention the bot and then reply to the bot's response to verify conversation continuity. Test an unlisted Telegram user and confirm no agent run is started. Pause the assistant in OpenDots and verify that an allowed request receives the paused notice. + +The adapter supports Telegram inline interactions and streamed replies through the Channels SDK. OpenDots uses the same runtime, agent permissions, and Intelligence conversation machinery as its other channels. + ## Calls The included speech adapter uses the Realtime API at `api.openai.com`. Set `VOICE_API_KEY` to a key with access to that API and `VOICE_MODEL` to a supported Realtime model (the local UI test used `gpt-realtime-2.1`); `VOICE_NAME` selects the voice. `OPENAI_BASE_URL` changes the compute model endpoint only, not speech. Calls use browser microphone access and WebRTC. Hosted deployments need HTTPS. The server mediates provider setup and delegates compute to the selected Dot's conversation. @@ -186,3 +238,13 @@ npm run build ``` Automated tests use service fixtures. Live model, Intelligence, Slack, and voice verification requires your own configured services. + +## Inbox and Watchers + +The Inbox collects completed and failed background-task outcomes, plus notifications when a Watcher detects a change. + +A Watcher monitors a public HTTP(S) URL at an interval from one minute to 24 hours. The first successful check establishes a baseline. Later content changes queue a normal OpenDots task in the selected conversation, so the existing Dot, tools, permissions, and task history handle the follow-up. + +Open **Scheduled & activity** to add a Watcher. Choose an existing conversation, enter the URL and investigation prompt, and choose the interval. Watchers can be paused, resumed, or deleted. + +Watcher requests reject local/private destinations, URL credentials, oversized responses, and excessive redirects. Keep remote OpenDots deployments protected with owner authentication and HTTPS. diff --git a/src/client/App.tsx b/src/client/App.tsx index 353b631..9fe6766 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -6,6 +6,7 @@ import { CopilotKitProvider } from '@copilotkit/react-core/v2'; import { ArrowUp, ArrowUpRight, + Bell, BookOpen, Clock3, Code2, @@ -45,9 +46,9 @@ export function App() { const [workspace, setWorkspace] = useState(); const [selectedDot, setSelectedDot] = useState(''); const [selectedThread, setSelectedThread] = useState(); - const [view, rawSetView] = useState<'chat' | 'tasks' | 'memories' | 'space'>( - 'chat', - ); + const [view, rawSetView] = useState< + 'chat' | 'tasks' | 'memories' | 'space' | 'inbox' + >('chat'); const dirtyPage = useRef(false); const [spaceId, setSpaceId] = useState(''); const [pageId, setPageId] = useState(); @@ -289,8 +290,8 @@ export function App() { > - )} - {view === 'memories' ? ( + {view === 'inbox' ? ( + <> +
+ {(state.inbox ?? []).map((item) => ( +
+
+ +
+
+ {item.title} + {new Date(item.createdAt).toLocaleString()} +

{item.body}

+
+ {item.taskId && ( + + )} + + +
+
+
+ ))} +
+ {!(state.inbox ?? []).length && ( +
+ +

Nothing waiting for you.

+

Completed work, failures, and watcher triggers appear here.

+
+ )} + + ) : view === 'memories' ? ( <>
{state.memories.map((memory) => ( @@ -773,6 +859,130 @@ export function App() { ) : ( <> +
+
+

Watchers

+ Queue a task when a public page changes. +
+ {workspace.conversations.length ? ( +
{ + event.preventDefault(); + const form = event.currentTarget; + const data = new FormData(form); + const minutes = Number(data.get('minutes') ?? 15); + const url = String(data.get('url') ?? '').trim(); + const prompt = String(data.get('prompt') ?? '').trim(); + const threadId = String(data.get('threadId') ?? ''); + if ( + !url || + !prompt || + !threadId || + !Number.isFinite(minutes) || + minutes < 1 || + minutes > 1440 + ) { + setError('Complete the watcher fields with a 1–1440 minute interval.'); + return; + } + const ok = await mutate('/watchers', 'POST', { + url, + prompt, + threadId, + intervalSeconds: Math.round(minutes * 60), + }); + if (ok) form.reset(); + }} + > + +