feat: Wire bot to real CoPilot streaming via bot chat proxy

Replaces the echo handler with actual CoPilot integration:

- platform-api.ts: now calls /api/platform-linking/chat/session and
  /api/platform-linking/chat/stream (bot API key auth, no user JWT)
- bot.ts: creates sessions on first message, streams CoPilot responses
  directly to the chat platform via thread.post(stream)
- Adds PLATFORM_BOT_API_KEY support in bot headers

The full flow now works:
  User messages bot → resolve user → create session → stream CoPilot → post response
This commit is contained in:
Bentlybro
2026-03-31 15:49:33 +00:00
parent 9eaa903978
commit 7ffcd704c9
2 changed files with 52 additions and 37 deletions

View File

@@ -190,26 +190,27 @@ async function handleCoPilotMessage(
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"}_`
);
try {
// Create a session if we don't have one
if (!sessionId) {
sessionId = await api.createChatSession(userId);
await thread.setState({ ...state, sessionId });
console.log(`[bot] Created session ${sessionId} for user ${userId.slice(-8)}`);
}
// Stream CoPilot response directly to the chat platform
const stream = api.streamChat(userId, text, sessionId);
await thread.post(stream);
} catch (err: any) {
console.error(`[bot] CoPilot error for user ${userId.slice(-8)}:`, err.message);
await thread.post(
"Sorry, I ran into an issue processing your message. Please try again."
);
}
}

View File

@@ -100,17 +100,24 @@ export class PlatformAPI {
}
/**
* Create a new CoPilot chat session for a user.
* Returns the session ID.
* Create a new CoPilot chat session for a linked user.
* Uses the bot chat proxy (no user JWT needed).
*/
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}`,
},
});
async createChatSession(userId: string): Promise<string> {
const res = await fetch(
`${this.baseUrl}/api/platform-linking/chat/session`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
...this.botHeaders(),
},
body: JSON.stringify({
user_id: userId,
message: "session_init",
}),
}
);
if (!res.ok) {
throw new Error(
@@ -119,30 +126,32 @@ export class PlatformAPI {
}
const data = await res.json();
return data.id;
return data.session_id;
}
/**
* Stream a chat message to CoPilot and yield text chunks.
* Uses SSE (Server-Sent Events) to stream the response.
* Stream a chat message to CoPilot on behalf of a linked user.
* Uses the bot chat proxy — authenticated via bot API key.
* Yields text chunks from the SSE stream.
*/
async *streamChat(
sessionId: string,
userId: string,
message: string,
userToken: string
sessionId?: string
): AsyncGenerator<string> {
const res = await fetch(
`${this.baseUrl}/api/chat/sessions/${sessionId}/stream`,
`${this.baseUrl}/api/platform-linking/chat/stream`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userToken}`,
Accept: "text/event-stream",
...this.botHeaders(),
},
body: JSON.stringify({
user_id: userId,
message,
is_user_message: true,
session_id: sessionId,
}),
}
);
@@ -168,7 +177,6 @@ export class PlatformAPI {
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) {
@@ -178,14 +186,12 @@ export class PlatformAPI {
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;
}
@@ -194,4 +200,12 @@ export class PlatformAPI {
}
}
}
private botHeaders(): Record<string, string> {
const key = process.env.PLATFORM_BOT_API_KEY;
if (key) {
return { "X-Bot-API-Key": key };
}
return {};
}
}