Skip to content

Commit 0f267b0

Browse files
authored
Merge pull request #19769 from Budibase/fix/teams-source-delivery
fix: consistent delivery of teams source urls
2 parents 1119d63 + 313c1fc commit 0f267b0

3 files changed

Lines changed: 94 additions & 57 deletions

File tree

‎packages/server/src/api/controllers/webhook/ms-teams.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ const createTeamsMessageHandler = ({
445445
export async function MSTeamsWebhook(
446446
ctx: Ctx<unknown, unknown, { instance: string; agentId: string }>
447447
) {
448+
let serviceUrl: string | undefined
448449
await runChatWebhook({
449450
ctx,
450451
providerName: "Teams",
@@ -457,7 +458,7 @@ export async function MSTeamsWebhook(
457458
) {
458459
throw new HTTPError("Missing Microsoft Teams service URL", 400)
459460
}
460-
validateMSTeamsServiceUrl(body.serviceUrl)
461+
serviceUrl = validateMSTeamsServiceUrl(body.serviceUrl)
461462
},
462463
createWebhookHandler: async ({ workspaceId, agentId }) => {
463464
const {
@@ -488,6 +489,8 @@ export async function MSTeamsWebhook(
488489
appPassword: integration.appPassword,
489490
appTenantId: integration.tenantId,
490491
appType: "SingleTenant",
492+
// Card posts use the adapter URL; DM streams use the activity URL.
493+
apiUrl: serviceUrl,
491494
}),
492495
},
493496
state: await getTeamsState(),

‎packages/server/src/api/controllers/webhook/runChatWebhook.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,11 @@ export const runChatWebhook = async ({
3131
return
3232
}
3333

34-
let handleWebhook: (request: Request) => Promise<Response>
34+
const rawBody = await readRawBody(ctx.req)
3535
try {
36-
handleWebhook = await createWebhookHandler({
37-
workspaceId: prodWorkspaceId,
38-
agentId: ctx.params.agentId,
39-
})
36+
if (ctx.get("authorization")) {
37+
validateBody?.(tryParseJson(rawBody))
38+
}
4039
} catch (error) {
4140
if (error instanceof HTTPError) {
4241
ctx.status = error.status
@@ -46,11 +45,12 @@ export const runChatWebhook = async ({
4645
throw error
4746
}
4847

49-
const rawBody = await readRawBody(ctx.req)
48+
let handleWebhook: (request: Request) => Promise<Response>
5049
try {
51-
if (ctx.get("authorization")) {
52-
validateBody?.(tryParseJson(rawBody))
53-
}
50+
handleWebhook = await createWebhookHandler({
51+
workspaceId: prodWorkspaceId,
52+
agentId: ctx.params.agentId,
53+
})
5454
} catch (error) {
5555
if (error instanceof HTTPError) {
5656
ctx.status = error.status

‎packages/server/src/api/routes/tests/ai/agentTeams.spec.ts‎

Lines changed: 81 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import os from "os"
6868
import path from "path"
6969

7070
import extract from "extract-zip"
71+
import { createTeamsAdapter } from "@chat-adapter/teams"
7172
import { context, docIds } from "@budibase/backend-core"
7273
import { generator } from "@budibase/backend-core/tests"
7374
import { ChatCommands } from "@budibase/shared-core"
@@ -87,6 +88,7 @@ import { webhookChat } from "../../../controllers/ai/chatConversations"
8788
const { getMockChatOptions, resetMockChatState, setMockPostEphemeralResult } =
8889
jest.requireActual("chat") as ChatMockModule
8990
const mockedWebhookChat = webhookChat as jest.MockedFunction<typeof webhookChat>
91+
const mockedCreateTeamsAdapter = jest.mocked(createTeamsAdapter)
9092
const mockedGetFileUrlForAgent = jest.mocked(sdk.ai.rag.getFileUrlForAgent)
9193
const TEAMS_APP_ID = generator.guid()
9294

@@ -110,6 +112,7 @@ describe("agent teams integration provisioning", () => {
110112
"test-config"
111113
)
112114
mockedWebhookChat.mockClear()
115+
mockedCreateTeamsAdapter.mockClear()
113116
mockedGetFileUrlForAgent.mockReset()
114117
resetMockChatState()
115118
})
@@ -524,6 +527,7 @@ describe("agent teams integration provisioning", () => {
524527

525528
expect(response.body.error).toEqual("Invalid Microsoft Teams service URL")
526529
expect(mockedWebhookChat).not.toHaveBeenCalled()
530+
expect(mockedCreateTeamsAdapter).not.toHaveBeenCalled()
527531
})
528532

529533
it(`returns a private link prompt for ${ChatCommands.LINK} and /${ChatCommands.LINK} commands`, async () => {
@@ -800,56 +804,86 @@ describe("agent teams integration provisioning", () => {
800804
expect(mockedWebhookChat).toHaveBeenCalledTimes(1)
801805
})
802806

803-
it("appends downloadable RAG source links to Teams personal replies", async () => {
804-
mockedGetFileUrlForAgent.mockResolvedValue(
805-
"/files/signed/prod-budi-app-assets/source.pdf"
806-
)
807-
mockedWebhookChat.mockResolvedValueOnce({
808-
messages: [
809-
{
810-
id: "assistant-1",
811-
role: "assistant",
812-
parts: [{ type: "text", text: "Answer with sources" }],
813-
},
814-
] as any,
815-
assistantText: "Answer with sources",
816-
ragSources: [
817-
{
818-
sourceId: "source-1",
819-
fileId: "file-1",
820-
filename: "Source [One]\n@Draft.pdf",
821-
},
822-
],
823-
title: "Mock conversation",
824-
})
807+
it.each([false, true])(
808+
"sends personal reply sources through the activity service URL (streaming: %s)",
809+
async streaming => {
810+
const serviceUrl = new URL(
811+
"/emea/",
812+
DEFAULT_MSTEAMS_SERVICE_URL
813+
).toString()
814+
mockedGetFileUrlForAgent.mockResolvedValue(
815+
"/files/signed/prod-budi-app-assets/source.pdf"
816+
)
817+
mockedWebhookChat.mockImplementationOnce(
818+
async ({ onAssistantStream }) => {
819+
if (streaming) {
820+
async function* sourceAnswerStream() {
821+
yield "Answer with "
822+
yield "sources"
823+
}
824+
await onAssistantStream!(sourceAnswerStream())
825+
}
826+
return {
827+
messages: [
828+
{
829+
id: "assistant-1",
830+
role: "assistant",
831+
parts: [{ type: "text", text: "Answer with sources" }],
832+
},
833+
],
834+
assistantText: "Answer with sources",
835+
allowKnowledgeSourceDownload: true,
836+
ragSources: [
837+
{
838+
sourceId: "source-1",
839+
fileId: "file-1",
840+
filename: "Source [One]\n@Draft.pdf",
841+
},
842+
],
843+
title: "Mock conversation",
844+
}
845+
}
846+
)
825847

826-
const { agent, linkExternalUser } = await setupProvisionedTeamsAgent()
827-
const path = `/api/webhooks/ms-teams/${config.getProdWorkspaceId()}/${agent._id}`
828-
await linkExternalUser("user-1")
848+
const { agent, linkExternalUser } = await setupProvisionedTeamsAgent()
849+
const path = `/api/webhooks/ms-teams/${config.getProdWorkspaceId()}/${agent._id}`
850+
await linkExternalUser("user-1")
829851

830-
const response = await postTeamsMessage({
831-
path,
832-
body: {
833-
id: "activity-rag-personal",
834-
type: "message",
835-
text: "hello teams",
836-
from: { id: "user-1", name: "Teams User" },
837-
conversation: { id: "conversation-1", conversationType: "personal" },
838-
channelData: { tenant: { id: "tenant-1" } },
839-
},
840-
})
852+
const response = await postTeamsMessage({
853+
path,
854+
body: {
855+
id: "activity-rag-personal",
856+
serviceUrl,
857+
type: "message",
858+
text: "hello teams",
859+
from: { id: "user-1", name: "Teams User" },
860+
conversation: {
861+
id: "conversation-1",
862+
conversationType: "personal",
863+
},
864+
channelData: { tenant: { id: "tenant-1" } },
865+
},
866+
})
841867

842-
expect(response.body.messages).toContain("Answer with sources")
843-
const cardMessage = response.body.messages.find((message: string) =>
844-
message.includes("Source One Draft.pdf")
845-
)
846-
expect(cardMessage).toContain('"title":"Sources"')
847-
expect(cardMessage).toContain(
848-
"http://localhost:10000/files/signed/prod-budi-app-assets/source.pdf"
849-
)
850-
expect(mockedWebhookChat).toHaveBeenCalledTimes(1)
851-
expect(mockedGetFileUrlForAgent).toHaveBeenCalledWith(agent._id, "file-1")
852-
})
868+
expect(response.body.messages).toHaveLength(2)
869+
expect(response.body.messages[0]).toEqual("Answer with sources")
870+
expect(mockedCreateTeamsAdapter).toHaveBeenLastCalledWith(
871+
expect.objectContaining({ apiUrl: serviceUrl })
872+
)
873+
const cardMessage = response.body.messages.find((message: string) =>
874+
message.includes("Source One Draft.pdf")
875+
)
876+
expect(cardMessage).toContain('"title":"Sources"')
877+
expect(cardMessage).toContain(
878+
"http://localhost:10000/files/signed/prod-budi-app-assets/source.pdf"
879+
)
880+
expect(mockedWebhookChat).toHaveBeenCalledTimes(1)
881+
expect(mockedGetFileUrlForAgent).toHaveBeenCalledWith(
882+
agent._id,
883+
"file-1"
884+
)
885+
}
886+
)
853887

854888
it("does not append RAG source links to Teams channel replies", async () => {
855889
mockedWebhookChat.mockResolvedValueOnce({

0 commit comments

Comments
 (0)