mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat: CoPilot bot service using Vercel Chat SDK
Multi-platform bot service that deploys CoPilot to Discord, Telegram, and Slack from a single codebase using Vercel's Chat SDK. ## What's included ### Core bot (src/bot.ts) - Chat SDK instance with dynamic adapter loading - onNewMention: resolves platform user → AutoGPT account - Unlinked users get a link prompt via the platform-linking API - Subscribed message handler with state management - MVP echo response (CoPilot API integration next) ### Platform API client (src/platform-api.ts) - Calls /api/platform-linking/resolve on every message - Creates link tokens for unlinked users - Checks link token status - Chat session creation and SSE streaming (prepared for CoPilot) ### Serverless routes (src/api/) - POST /api/webhooks/discord — Discord interactions endpoint - POST /api/webhooks/telegram — Telegram updates - POST /api/webhooks/slack — Slack events - GET /api/gateway/discord — Gateway cron for Discord messages ### Standalone mode (src/index.ts) - Long-running process for Docker/PM2 deployment - Auto-detects enabled adapters from env vars - Redis or in-memory state ## Stacked on - feat/platform-bot-linking (PR #12615)
This commit is contained in:
24
autogpt_platform/copilot-bot/.env.example
Normal file
24
autogpt_platform/copilot-bot/.env.example
Normal file
@@ -0,0 +1,24 @@
|
||||
# ── AutoGPT Platform API ──────────────────────────────────────────────
|
||||
AUTOGPT_API_URL=http://localhost:8006
|
||||
# Service API key for bot-facing endpoints (TODO: implement in backend)
|
||||
# AUTOGPT_BOT_API_KEY=
|
||||
|
||||
# ── Discord ───────────────────────────────────────────────────────────
|
||||
DISCORD_BOT_TOKEN=
|
||||
DISCORD_PUBLIC_KEY=
|
||||
DISCORD_APPLICATION_ID=
|
||||
# Optional: comma-separated role IDs that trigger mentions
|
||||
# DISCORD_MENTION_ROLE_IDS=
|
||||
|
||||
# ── Telegram ──────────────────────────────────────────────────────────
|
||||
# TELEGRAM_BOT_TOKEN=
|
||||
|
||||
# ── Slack ─────────────────────────────────────────────────────────────
|
||||
# SLACK_BOT_TOKEN=
|
||||
# SLACK_SIGNING_SECRET=
|
||||
# SLACK_APP_TOKEN=
|
||||
|
||||
# ── State ─────────────────────────────────────────────────────────────
|
||||
# For production, use Redis:
|
||||
# REDIS_URL=redis://localhost:6379
|
||||
# For development, in-memory state is used by default.
|
||||
4
autogpt_platform/copilot-bot/.gitignore
vendored
Normal file
4
autogpt_platform/copilot-bot/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.log
|
||||
73
autogpt_platform/copilot-bot/README.md
Normal file
73
autogpt_platform/copilot-bot/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# CoPilot Bot
|
||||
|
||||
Multi-platform bot service for AutoGPT CoPilot, built with [Vercel Chat SDK](https://chat-sdk.dev).
|
||||
|
||||
Deploys CoPilot to Discord, Telegram, Slack, and more from a single codebase.
|
||||
|
||||
## How it works
|
||||
|
||||
1. User messages the bot on any platform (Discord, Telegram, Slack)
|
||||
2. Bot checks if the platform user is linked to an AutoGPT account
|
||||
3. If not linked → sends a one-time link URL
|
||||
4. User clicks → logs in to AutoGPT → accounts are linked
|
||||
5. Future messages are forwarded to CoPilot and responses streamed back
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Copy env template
|
||||
cp .env.example .env
|
||||
|
||||
# Configure at least one platform adapter (e.g. Discord)
|
||||
# Edit .env with your bot tokens
|
||||
|
||||
# Run in development
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.ts # Standalone entry point
|
||||
├── config.ts # Environment-based configuration
|
||||
├── bot.ts # Core bot logic (Chat SDK handlers)
|
||||
├── platform-api.ts # AutoGPT platform API client
|
||||
└── api/ # Serverless API routes (Vercel)
|
||||
├── _bot.ts # Singleton bot instance
|
||||
├── webhooks/ # Platform webhook endpoints
|
||||
│ ├── discord.ts
|
||||
│ ├── telegram.ts
|
||||
│ └── slack.ts
|
||||
└── gateway/
|
||||
└── discord.ts # Gateway cron for Discord messages
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Standalone (Docker/PM2)
|
||||
```bash
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
### Serverless (Vercel)
|
||||
Deploy to Vercel. Webhook URLs:
|
||||
- Discord: `https://your-app.vercel.app/api/webhooks/discord`
|
||||
- Telegram: `https://your-app.vercel.app/api/webhooks/telegram`
|
||||
- Slack: `https://your-app.vercel.app/api/webhooks/slack`
|
||||
|
||||
For Discord messages (Gateway), add a cron job in `vercel.json`:
|
||||
```json
|
||||
{
|
||||
"crons": [{ "path": "/api/gateway/discord", "schedule": "*/9 * * * *" }]
|
||||
}
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- [Chat SDK](https://chat-sdk.dev) — Cross-platform bot abstraction
|
||||
- AutoGPT Platform API — Account linking + CoPilot chat
|
||||
27
autogpt_platform/copilot-bot/package.json
Normal file
27
autogpt_platform/copilot-bot/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@autogpt/copilot-bot",
|
||||
"version": "0.1.0",
|
||||
"description": "Multi-platform CoPilot bot service using Vercel Chat SDK",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"start": "tsx src/index.ts",
|
||||
"build": "tsc",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"chat": "^4.23.0",
|
||||
"@chat-adapter/discord": "^4.23.0",
|
||||
"@chat-adapter/telegram": "^4.23.0",
|
||||
"@chat-adapter/slack": "^4.23.0",
|
||||
"@chat-adapter/state-memory": "^4.23.0",
|
||||
"@chat-adapter/state-redis": "^4.23.0",
|
||||
"dotenv": "^16.4.0",
|
||||
"eventsource-parser": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
33
autogpt_platform/copilot-bot/src/api/_bot.ts
Normal file
33
autogpt_platform/copilot-bot/src/api/_bot.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Singleton bot instance for serverless environments.
|
||||
*
|
||||
* In serverless (Vercel), each request may hit a cold or warm instance.
|
||||
* We create the bot once per instance and reuse it across requests.
|
||||
*/
|
||||
|
||||
import { loadConfig } from "../config.js";
|
||||
import { createBot } from "../bot.js";
|
||||
import type { Chat } from "chat";
|
||||
|
||||
let _bot: ReturnType<typeof createBot> | null = null;
|
||||
|
||||
export function getBotInstance() {
|
||||
if (!_bot) {
|
||||
const config = loadConfig();
|
||||
|
||||
// In serverless, always use in-memory state unless Redis is configured
|
||||
let stateAdapter;
|
||||
if (config.redisUrl) {
|
||||
const { createRedisState } = require("@chat-adapter/state-redis");
|
||||
stateAdapter = createRedisState({ url: config.redisUrl });
|
||||
} else {
|
||||
const { createMemoryState } = require("@chat-adapter/state-memory");
|
||||
stateAdapter = createMemoryState();
|
||||
}
|
||||
|
||||
_bot = createBot(config, stateAdapter);
|
||||
console.log("[bot] Instance created (serverless)");
|
||||
}
|
||||
|
||||
return _bot;
|
||||
}
|
||||
39
autogpt_platform/copilot-bot/src/api/gateway/discord.ts
Normal file
39
autogpt_platform/copilot-bot/src/api/gateway/discord.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Discord Gateway cron endpoint.
|
||||
*
|
||||
* In serverless environments, Discord's Gateway WebSocket needs a
|
||||
* persistent connection to receive messages. This endpoint is called
|
||||
* by a cron job every 9 minutes to maintain the connection.
|
||||
*/
|
||||
|
||||
import { getBotInstance } from "../_bot.js";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
// Verify cron secret in production
|
||||
const authHeader = request.headers.get("authorization");
|
||||
if (
|
||||
process.env.CRON_SECRET &&
|
||||
authHeader !== `Bearer ${process.env.CRON_SECRET}`
|
||||
) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const bot = getBotInstance();
|
||||
await bot.initialize();
|
||||
|
||||
const discord = bot.getAdapter("discord");
|
||||
if (!discord) {
|
||||
return new Response("Discord adapter not configured", { status: 404 });
|
||||
}
|
||||
|
||||
const baseUrl = process.env.WEBHOOK_BASE_URL ?? "http://localhost:3000";
|
||||
const webhookUrl = `${baseUrl}/api/webhooks/discord`;
|
||||
const durationMs = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
return (discord as any).startGatewayListener(
|
||||
{},
|
||||
durationMs,
|
||||
undefined,
|
||||
webhookUrl
|
||||
);
|
||||
}
|
||||
22
autogpt_platform/copilot-bot/src/api/webhooks/discord.ts
Normal file
22
autogpt_platform/copilot-bot/src/api/webhooks/discord.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Discord webhook endpoint.
|
||||
*
|
||||
* Receives HTTP Interactions from Discord (button clicks, slash commands,
|
||||
* verification pings). For regular messages, the Gateway WebSocket is
|
||||
* used via the adapter's built-in connection.
|
||||
*
|
||||
* Deploy as: POST /api/webhooks/discord
|
||||
*/
|
||||
|
||||
import { getBotInstance } from "../_bot.js";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const bot = getBotInstance();
|
||||
const handler = bot.webhooks.discord;
|
||||
|
||||
if (!handler) {
|
||||
return new Response("Discord adapter not configured", { status: 404 });
|
||||
}
|
||||
|
||||
return handler(request);
|
||||
}
|
||||
17
autogpt_platform/copilot-bot/src/api/webhooks/slack.ts
Normal file
17
autogpt_platform/copilot-bot/src/api/webhooks/slack.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Slack webhook endpoint.
|
||||
* Deploy as: POST /api/webhooks/slack
|
||||
*/
|
||||
|
||||
import { getBotInstance } from "../_bot.js";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const bot = getBotInstance();
|
||||
const handler = bot.webhooks.slack;
|
||||
|
||||
if (!handler) {
|
||||
return new Response("Slack adapter not configured", { status: 404 });
|
||||
}
|
||||
|
||||
return handler(request);
|
||||
}
|
||||
17
autogpt_platform/copilot-bot/src/api/webhooks/telegram.ts
Normal file
17
autogpt_platform/copilot-bot/src/api/webhooks/telegram.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Telegram webhook endpoint.
|
||||
* Deploy as: POST /api/webhooks/telegram
|
||||
*/
|
||||
|
||||
import { getBotInstance } from "../_bot.js";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const bot = getBotInstance();
|
||||
const handler = bot.webhooks.telegram;
|
||||
|
||||
if (!handler) {
|
||||
return new Response("Telegram adapter not configured", { status: 404 });
|
||||
}
|
||||
|
||||
return handler(request);
|
||||
}
|
||||
215
autogpt_platform/copilot-bot/src/bot.ts
Normal file
215
autogpt_platform/copilot-bot/src/bot.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* CoPilot Bot — Multi-platform bot using Vercel Chat SDK.
|
||||
*
|
||||
* Handles:
|
||||
* - Account linking (prompts unlinked users to link)
|
||||
* - Message routing to CoPilot API
|
||||
* - Streaming responses back to the user
|
||||
*/
|
||||
|
||||
import { Chat } from "chat";
|
||||
import type { StateAdapter } from "chat";
|
||||
import { PlatformAPI } from "./platform-api.js";
|
||||
import type { Config } from "./config.js";
|
||||
|
||||
// Thread state persisted across messages
|
||||
export interface BotThreadState {
|
||||
/** Linked AutoGPT user ID */
|
||||
userId?: string;
|
||||
/** CoPilot chat session ID for this thread */
|
||||
sessionId?: string;
|
||||
/** Pending link token (if user hasn't linked yet) */
|
||||
pendingLinkToken?: string;
|
||||
}
|
||||
|
||||
export function createBot(config: Config, stateAdapter: StateAdapter) {
|
||||
const api = new PlatformAPI(config.autogptApiUrl);
|
||||
|
||||
// Build adapters based on config
|
||||
const adapters: Record<string, any> = {};
|
||||
|
||||
if (config.discord) {
|
||||
// Dynamic import to avoid loading unused adapters
|
||||
const { createDiscordAdapter } = require("@chat-adapter/discord");
|
||||
adapters.discord = createDiscordAdapter();
|
||||
}
|
||||
|
||||
if (config.telegram) {
|
||||
const { createTelegramAdapter } = require("@chat-adapter/telegram");
|
||||
adapters.telegram = createTelegramAdapter();
|
||||
}
|
||||
|
||||
if (config.slack) {
|
||||
const { createSlackAdapter } = require("@chat-adapter/slack");
|
||||
adapters.slack = createSlackAdapter();
|
||||
}
|
||||
|
||||
if (Object.keys(adapters).length === 0) {
|
||||
throw new Error(
|
||||
"No adapters enabled. Set at least one of: " +
|
||||
"DISCORD_BOT_TOKEN, TELEGRAM_BOT_TOKEN, SLACK_BOT_TOKEN"
|
||||
);
|
||||
}
|
||||
|
||||
const bot = new Chat<typeof adapters, BotThreadState>({
|
||||
userName: "copilot",
|
||||
adapters,
|
||||
state: stateAdapter,
|
||||
streamingUpdateIntervalMs: 500,
|
||||
fallbackStreamingPlaceholderText: "Thinking...",
|
||||
});
|
||||
|
||||
// ── New mention (first message in a thread) ──────────────────────
|
||||
|
||||
bot.onNewMention(async (thread, message) => {
|
||||
const adapterName = getAdapterName(thread);
|
||||
const platformUserId = message.author.userId;
|
||||
|
||||
console.log(
|
||||
`[bot] New mention from ${adapterName}:${platformUserId} in ${thread.id}`
|
||||
);
|
||||
|
||||
// Check if user is linked
|
||||
const resolved = await api.resolve(adapterName, platformUserId);
|
||||
|
||||
if (!resolved.linked) {
|
||||
await handleUnlinkedUser(thread, message, adapterName, api);
|
||||
return;
|
||||
}
|
||||
|
||||
// User is linked — subscribe and handle the message
|
||||
await thread.subscribe();
|
||||
await thread.setState({ userId: resolved.user_id });
|
||||
|
||||
await handleCoPilotMessage(thread, message.text, resolved.user_id!, api);
|
||||
});
|
||||
|
||||
// ── Subscribed messages (follow-ups in a thread) ─────────────────
|
||||
|
||||
bot.onSubscribedMessage(async (thread, message) => {
|
||||
const state = await thread.state;
|
||||
|
||||
if (!state?.userId) {
|
||||
// Somehow lost state — re-resolve
|
||||
const adapterName = getAdapterName(thread);
|
||||
const resolved = await api.resolve(adapterName, message.author.userId);
|
||||
|
||||
if (!resolved.linked) {
|
||||
await handleUnlinkedUser(thread, message, adapterName, api);
|
||||
return;
|
||||
}
|
||||
|
||||
await thread.setState({ userId: resolved.user_id });
|
||||
await handleCoPilotMessage(
|
||||
thread,
|
||||
message.text,
|
||||
resolved.user_id!,
|
||||
api
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await handleCoPilotMessage(thread, message.text, state.userId, api);
|
||||
});
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get the adapter/platform name from a thread.
|
||||
*/
|
||||
function getAdapterName(thread: any): string {
|
||||
// Thread ID format is "adapter:channel:thread"
|
||||
const parts = thread.id.split(":");
|
||||
return parts[0] ?? "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an unlinked user — create a link token and send them a prompt.
|
||||
*/
|
||||
async function handleUnlinkedUser(
|
||||
thread: any,
|
||||
message: any,
|
||||
platform: string,
|
||||
api: PlatformAPI
|
||||
) {
|
||||
console.log(
|
||||
`[bot] Unlinked user ${platform}:${message.author.userId}, sending link prompt`
|
||||
);
|
||||
|
||||
try {
|
||||
const linkResult = await api.createLinkToken({
|
||||
platform,
|
||||
platformUserId: message.author.userId,
|
||||
platformUsername: message.author.fullName ?? message.author.username,
|
||||
});
|
||||
|
||||
await thread.post(
|
||||
`👋 To use CoPilot, link your AutoGPT account first.\n\n` +
|
||||
`🔗 **Link your account:** ${linkResult.link_url}\n\n` +
|
||||
`_This link expires in 30 minutes._`
|
||||
);
|
||||
|
||||
// Store the pending token so we could poll later if needed
|
||||
await thread.setState({ pendingLinkToken: linkResult.token });
|
||||
} catch (err: any) {
|
||||
if (err.message?.includes("409")) {
|
||||
// Already linked (race condition) — retry resolve
|
||||
const resolved = await api.resolve(platform, message.author.userId);
|
||||
if (resolved.linked) {
|
||||
await thread.subscribe();
|
||||
await thread.setState({ userId: resolved.user_id });
|
||||
await handleCoPilotMessage(
|
||||
thread,
|
||||
message.text,
|
||||
resolved.user_id!,
|
||||
api
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.error("[bot] Failed to create link token:", err);
|
||||
await thread.post(
|
||||
"Sorry, I couldn't set up account linking right now. Please try again later."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward a message to CoPilot and stream the response back.
|
||||
*/
|
||||
async function handleCoPilotMessage(
|
||||
thread: any,
|
||||
text: string,
|
||||
userId: string,
|
||||
api: PlatformAPI
|
||||
) {
|
||||
const state = await thread.state;
|
||||
let sessionId = state?.sessionId;
|
||||
|
||||
// TODO: For now, we need a way to get a user token to call the chat API.
|
||||
// This will require either:
|
||||
// 1. A service-to-service token exchange endpoint
|
||||
// 2. Storing user tokens during the linking flow
|
||||
// 3. A bot-specific chat endpoint that accepts user_id directly
|
||||
//
|
||||
// For the MVP, we'll echo back to prove the pipeline works.
|
||||
// The CoPilot integration comes in the next iteration.
|
||||
|
||||
console.log(
|
||||
`[bot] Message from user ${userId.slice(-8)}: ${text.slice(0, 100)}`
|
||||
);
|
||||
|
||||
await thread.startTyping();
|
||||
|
||||
// MVP: Echo back with user info to prove linking works
|
||||
await thread.post(
|
||||
`✅ **Connected as AutoGPT user** \`${userId.slice(-8)}\`\n\n` +
|
||||
`> ${text}\n\n` +
|
||||
`_CoPilot integration coming soon. ` +
|
||||
`Session: ${sessionId ?? "new"}_`
|
||||
);
|
||||
}
|
||||
28
autogpt_platform/copilot-bot/src/config.ts
Normal file
28
autogpt_platform/copilot-bot/src/config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import "dotenv/config";
|
||||
|
||||
export interface Config {
|
||||
/** AutoGPT platform API base URL */
|
||||
autogptApiUrl: string;
|
||||
|
||||
/** Whether each adapter is enabled (based on env vars being set) */
|
||||
discord: boolean;
|
||||
telegram: boolean;
|
||||
slack: boolean;
|
||||
|
||||
/** Use Redis for state (production) or in-memory (dev) */
|
||||
redisUrl?: string;
|
||||
}
|
||||
|
||||
export function loadConfig(): Config {
|
||||
return {
|
||||
autogptApiUrl: env("AUTOGPT_API_URL", "http://localhost:8006"),
|
||||
discord: !!process.env.DISCORD_BOT_TOKEN,
|
||||
telegram: !!process.env.TELEGRAM_BOT_TOKEN,
|
||||
slack: !!process.env.SLACK_BOT_TOKEN,
|
||||
redisUrl: process.env.REDIS_URL,
|
||||
};
|
||||
}
|
||||
|
||||
function env(key: string, fallback: string): string {
|
||||
return process.env[key] ?? fallback;
|
||||
}
|
||||
58
autogpt_platform/copilot-bot/src/index.ts
Normal file
58
autogpt_platform/copilot-bot/src/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* CoPilot Bot — Entry point.
|
||||
*
|
||||
* Loads config, creates adapters, starts the bot.
|
||||
* For serverless deployment (Vercel), see src/api/ routes.
|
||||
* This file is for standalone / long-running deployment.
|
||||
*/
|
||||
|
||||
import { loadConfig } from "./config.js";
|
||||
import { createBot } from "./bot.js";
|
||||
|
||||
async function main() {
|
||||
console.log("🤖 CoPilot Bot starting...\n");
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
// Log which adapters are enabled
|
||||
const enabled = [
|
||||
config.discord && "Discord",
|
||||
config.telegram && "Telegram",
|
||||
config.slack && "Slack",
|
||||
].filter(Boolean);
|
||||
|
||||
console.log(`📡 Adapters: ${enabled.join(", ") || "none"}`);
|
||||
console.log(`🔗 API: ${config.autogptApiUrl}`);
|
||||
console.log(`💾 State: ${config.redisUrl ? "Redis" : "In-memory"}\n`);
|
||||
|
||||
// Create state adapter
|
||||
let stateAdapter;
|
||||
if (config.redisUrl) {
|
||||
const { createRedisState } = await import("@chat-adapter/state-redis");
|
||||
stateAdapter = createRedisState({ url: config.redisUrl });
|
||||
} else {
|
||||
const { createMemoryState } = await import("@chat-adapter/state-memory");
|
||||
stateAdapter = createMemoryState();
|
||||
}
|
||||
|
||||
// Create and start the bot
|
||||
const bot = createBot(config, stateAdapter);
|
||||
|
||||
console.log("✅ CoPilot Bot ready.\n");
|
||||
|
||||
// Keep the process alive
|
||||
process.on("SIGINT", () => {
|
||||
console.log("\n🛑 Shutting down...");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
console.log("\n🛑 Shutting down...");
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
197
autogpt_platform/copilot-bot/src/platform-api.ts
Normal file
197
autogpt_platform/copilot-bot/src/platform-api.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Client for the AutoGPT Platform Linking & Chat APIs.
|
||||
*
|
||||
* Handles:
|
||||
* - Resolving platform users → AutoGPT accounts
|
||||
* - Creating link tokens for unlinked users
|
||||
* - Checking link token status
|
||||
* - Creating chat sessions and streaming messages
|
||||
*/
|
||||
|
||||
export interface ResolveResult {
|
||||
linked: boolean;
|
||||
user_id?: string;
|
||||
platform_username?: string;
|
||||
}
|
||||
|
||||
export interface LinkTokenResult {
|
||||
token: string;
|
||||
expires_at: string;
|
||||
link_url: string;
|
||||
}
|
||||
|
||||
export interface LinkTokenStatus {
|
||||
status: "pending" | "linked" | "expired";
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
export class PlatformAPI {
|
||||
constructor(private baseUrl: string) {}
|
||||
|
||||
/**
|
||||
* Check if a platform user is linked to an AutoGPT account.
|
||||
*/
|
||||
async resolve(
|
||||
platform: string,
|
||||
platformUserId: string
|
||||
): Promise<ResolveResult> {
|
||||
const res = await fetch(`${this.baseUrl}/api/platform-linking/resolve`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
platform: platform.toUpperCase(),
|
||||
platform_user_id: platformUserId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Platform resolve failed: ${res.status} ${await res.text()}`
|
||||
);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a link token for an unlinked platform user.
|
||||
*/
|
||||
async createLinkToken(params: {
|
||||
platform: string;
|
||||
platformUserId: string;
|
||||
platformUsername?: string;
|
||||
channelId?: string;
|
||||
}): Promise<LinkTokenResult> {
|
||||
const res = await fetch(`${this.baseUrl}/api/platform-linking/tokens`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
platform: params.platform.toUpperCase(),
|
||||
platform_user_id: params.platformUserId,
|
||||
platform_username: params.platformUsername,
|
||||
channel_id: params.channelId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Create link token failed: ${res.status} ${await res.text()}`
|
||||
);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a link token has been consumed.
|
||||
*/
|
||||
async getLinkTokenStatus(token: string): Promise<LinkTokenStatus> {
|
||||
const res = await fetch(
|
||||
`${this.baseUrl}/api/platform-linking/tokens/${token}/status`
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Link token status failed: ${res.status} ${await res.text()}`
|
||||
);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new CoPilot chat session for a user.
|
||||
* Returns the session ID.
|
||||
*/
|
||||
async createChatSession(userToken: string): Promise<string> {
|
||||
const res = await fetch(`${this.baseUrl}/api/chat/sessions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${userToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Create chat session failed: ${res.status} ${await res.text()}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return data.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream a chat message to CoPilot and yield text chunks.
|
||||
* Uses SSE (Server-Sent Events) to stream the response.
|
||||
*/
|
||||
async *streamChat(
|
||||
sessionId: string,
|
||||
message: string,
|
||||
userToken: string
|
||||
): AsyncGenerator<string> {
|
||||
const res = await fetch(
|
||||
`${this.baseUrl}/api/chat/sessions/${sessionId}/stream`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${userToken}`,
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
is_user_message: true,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Stream chat failed: ${res.status} ${await res.text()}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!res.body) {
|
||||
throw new Error("No response body for SSE stream");
|
||||
}
|
||||
|
||||
// Parse SSE stream
|
||||
const decoder = new TextDecoder();
|
||||
const reader = res.body.getReader();
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
// Keep the last potentially incomplete line in the buffer
|
||||
buffer = lines.pop() ?? "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const data = line.slice(6).trim();
|
||||
if (data === "[DONE]") return;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
// Extract text content from SSE events
|
||||
if (parsed.type === "text" && parsed.content) {
|
||||
yield parsed.content;
|
||||
} else if (typeof parsed === "string") {
|
||||
yield parsed;
|
||||
}
|
||||
} catch {
|
||||
// Non-JSON data line, yield as-is if it has content
|
||||
if (data && data !== "[DONE]") {
|
||||
yield data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
autogpt_platform/copilot-bot/tsconfig.json
Normal file
19
autogpt_platform/copilot-bot/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "chat"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user