improvement(ms-teams): resolve mentions accurately

This commit is contained in:
Vikhyath Mondreti
2025-10-29 10:00:42 -07:00
parent 807014a5d2
commit 272b98955d
5 changed files with 298 additions and 8 deletions
@@ -4,6 +4,7 @@ import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createLogger } from '@/lib/logs/console/logger'
import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing'
import { generateRequestId } from '@/lib/utils'
import { resolveMentionsForChannel } from '@/tools/microsoft_teams/utils'
export const dynamic = 'force-dynamic'
@@ -140,23 +141,62 @@ export async function POST(request: NextRequest) {
}
let messageContent = validatedData.content
let contentType: 'text' | 'html' = 'text'
const mentionEntities: Array<{
type: 'mention'
mentioned: { id: string; name: string }
text: string
}> = []
try {
const mentionResult = await resolveMentionsForChannel(
validatedData.content,
validatedData.teamId,
validatedData.channelId,
validatedData.accessToken
)
if (mentionResult.hasMentions) {
contentType = 'html'
mentionEntities.push(...mentionResult.entities)
logger.info(`[${requestId}] Resolved ${mentionResult.entities.length} mention(s)`)
}
} catch (error) {
logger.warn(`[${requestId}] Failed to resolve mentions, continuing without them:`, error)
}
if (attachments.length > 0) {
contentType = 'html'
const attachmentTags = attachments
.map((att) => `<attachment id="${att.id}"></attachment>`)
.join(' ')
messageContent = `${validatedData.content}<br/>${attachmentTags}`
}
const messageBody = {
const messageBody: {
body: {
contentType: attachments.length > 0 ? 'html' : 'text',
contentType: 'text' | 'html'
content: string
}
attachments?: any[]
mentions?: Array<{
type: 'mention'
mentioned: { id: string; name: string }
text: string
}>
} = {
body: {
contentType,
content: messageContent,
},
}
if (attachments.length > 0) {
;(messageBody as any).attachments = attachments
messageBody.attachments = attachments
}
if (mentionEntities.length > 0) {
messageBody.mentions = mentionEntities
}
logger.info(`[${requestId}] Sending message to Teams channel: ${validatedData.channelId}`)
@@ -4,6 +4,7 @@ import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createLogger } from '@/lib/logs/console/logger'
import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing'
import { generateRequestId } from '@/lib/utils'
import { resolveMentionsForChat } from '@/tools/microsoft_teams/utils'
export const dynamic = 'force-dynamic'
@@ -138,23 +139,61 @@ export async function POST(request: NextRequest) {
}
let messageContent = validatedData.content
let contentType: 'text' | 'html' = 'text'
const mentionEntities: Array<{
type: 'mention'
mentioned: { id: string; name: string }
text: string
}> = []
try {
const mentionResult = await resolveMentionsForChat(
validatedData.content,
validatedData.chatId,
validatedData.accessToken
)
if (mentionResult.hasMentions) {
contentType = 'html'
mentionEntities.push(...mentionResult.entities)
logger.info(`[${requestId}] Resolved ${mentionResult.entities.length} mention(s)`)
}
} catch (error) {
logger.warn(`[${requestId}] Failed to resolve mentions, continuing without them:`, error)
}
if (attachments.length > 0) {
contentType = 'html'
const attachmentTags = attachments
.map((att) => `<attachment id="${att.id}"></attachment>`)
.join(' ')
messageContent = `${validatedData.content}<br/>${attachmentTags}`
}
const messageBody = {
const messageBody: {
body: {
contentType: attachments.length > 0 ? 'html' : 'text',
contentType: 'text' | 'html'
content: string
}
attachments?: any[]
mentions?: Array<{
type: 'mention'
mentioned: { id: string; name: string }
text: string
}>
} = {
body: {
contentType,
content: messageContent,
},
}
if (attachments.length > 0) {
;(messageBody as any).attachments = attachments
messageBody.attachments = attachments
}
if (mentionEntities.length > 0) {
messageBody.mentions = mentionEntities
}
logger.info(`[${requestId}] Sending message to Teams chat: ${validatedData.chatId}`)
+180
View File
@@ -4,6 +4,28 @@ import type { ToolFileData } from '@/tools/types'
const logger = createLogger('MicrosoftTeamsUtils')
interface ParsedMention {
name: string
fullTag: string
startIndex: number
endIndex: number
}
interface TeamMember {
id: string
displayName: string
userIdentityType?: string
}
export interface MentionEntity {
type: 'mention'
mentioned: {
id: string
name: string
}
text: string
}
/**
* Transform raw attachment data from Microsoft Graph API
*/
@@ -99,3 +121,161 @@ export async function fetchHostedContentsForChannelMessage(params: {
return []
}
}
function parseMentions(content: string): ParsedMention[] {
const mentions: ParsedMention[] = []
const mentionRegex = /<at>([^<]+)<\/at>/gi
let match: RegExpExecArray | null
while ((match = mentionRegex.exec(content)) !== null) {
const name = match[1].trim()
if (name) {
mentions.push({
name,
fullTag: match[0],
startIndex: match.index,
endIndex: match.index + match[0].length,
})
}
}
return mentions
}
async function fetchChatMembers(chatId: string, accessToken: string): Promise<TeamMember[]> {
const response = await fetch(
`https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/members`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (!response.ok) {
return []
}
const data = await response.json()
return (data.value || []).map((member: TeamMember) => ({
id: member.id,
displayName: member.displayName || '',
userIdentityType: member.userIdentityType,
}))
}
async function fetchChannelMembers(
teamId: string,
channelId: string,
accessToken: string
): Promise<TeamMember[]> {
const response = await fetch(
`https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/members`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (!response.ok) {
return []
}
const data = await response.json()
return (data.value || []).map((member: TeamMember) => ({
id: member.id,
displayName: member.displayName || '',
userIdentityType: member.userIdentityType,
}))
}
function findMemberByName(members: TeamMember[], name: string): TeamMember | undefined {
const normalizedName = name.trim().toLowerCase()
return members.find((member) => member.displayName.toLowerCase() === normalizedName)
}
export async function resolveMentionsForChat(
content: string,
chatId: string,
accessToken: string
): Promise<{ entities: MentionEntity[]; hasMentions: boolean }> {
const parsedMentions = parseMentions(content)
if (parsedMentions.length === 0) {
return { entities: [], hasMentions: false }
}
const members = await fetchChatMembers(chatId, accessToken)
const entities: MentionEntity[] = []
const resolvedNames = new Set<string>()
for (const mention of parsedMentions) {
if (resolvedNames.has(mention.fullTag)) {
continue
}
const member = findMemberByName(members, mention.name)
if (member) {
entities.push({
type: 'mention',
mentioned: {
id: member.id,
name: member.displayName,
},
text: mention.fullTag,
})
resolvedNames.add(mention.fullTag)
}
}
return {
entities,
hasMentions: entities.length > 0,
}
}
export async function resolveMentionsForChannel(
content: string,
teamId: string,
channelId: string,
accessToken: string
): Promise<{ entities: MentionEntity[]; hasMentions: boolean }> {
const parsedMentions = parseMentions(content)
if (parsedMentions.length === 0) {
return { entities: [], hasMentions: false }
}
const members = await fetchChannelMembers(teamId, channelId, accessToken)
const entities: MentionEntity[] = []
const resolvedNames = new Set<string>()
for (const mention of parsedMentions) {
if (resolvedNames.has(mention.fullTag)) {
continue
}
const member = findMemberByName(members, mention.name)
if (member) {
entities.push({
type: 'mention',
mentioned: {
id: member.id,
name: member.displayName,
},
text: mention.fullTag,
})
resolvedNames.add(mention.fullTag)
}
}
return {
entities,
hasMentions: entities.length > 0,
}
}
@@ -73,6 +73,12 @@ export const writeChannelTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTea
return '/api/tools/microsoft_teams/write_channel'
}
// If content contains mentions, use custom API route for mention resolution
const hasMentions = /<at>[^<]+<\/at>/i.test(params.content || '')
if (hasMentions) {
return '/api/tools/microsoft_teams/write_channel'
}
const encodedTeamId = encodeURIComponent(teamId)
const encodedChannelId = encodeURIComponent(channelId)
@@ -98,7 +104,8 @@ export const writeChannelTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTea
throw new Error('Content is required')
}
// If using custom API route (with files), pass all params
// If using custom API route (with files or mentions), pass all params
const hasMentions = /<at>[^<]+<\/at>/i.test(params.content || '')
if (params.files && params.files.length > 0) {
return {
accessToken: params.accessToken,
@@ -109,6 +116,15 @@ export const writeChannelTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTea
}
}
if (hasMentions) {
return {
accessToken: params.accessToken,
teamId: params.teamId,
channelId: params.channelId,
content: params.content,
}
}
// Microsoft Teams API expects this specific format for channel messages
const requestBody = {
body: {
+16 -1
View File
@@ -62,6 +62,12 @@ export const writeChatTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsW
return '/api/tools/microsoft_teams/write_chat'
}
// If content contains mentions, use custom API route for mention resolution
const hasMentions = /<at>[^<]+<\/at>/i.test(params.content || '')
if (hasMentions) {
return '/api/tools/microsoft_teams/write_chat'
}
return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages`
},
method: 'POST',
@@ -82,7 +88,8 @@ export const writeChatTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsW
throw new Error('Content is required')
}
// If using custom API route (with files), pass all params
// If using custom API route (with files or mentions), pass all params
const hasMentions = /<at>[^<]+<\/at>/i.test(params.content || '')
if (params.files && params.files.length > 0) {
return {
accessToken: params.accessToken,
@@ -92,6 +99,14 @@ export const writeChatTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsW
}
}
if (hasMentions) {
return {
accessToken: params.accessToken,
chatId: params.chatId,
content: params.content,
}
}
// Microsoft Teams API expects this specific format
const requestBody = {
body: {