@@ -3,6 +3,7 @@ namespace HelloWorldA365.AgentLogic.ResponsesApi;
33using Azure . Core ;
44using Azure . Identity ;
55using HelloWorldA365 . Models ;
6+ using Microsoft . Agents . A365 . Notifications ;
67using Microsoft . Agents . A365 . Notifications . Models ;
78using Microsoft . Agents . Builder ;
89using Microsoft . Agents . Builder . State ;
@@ -117,10 +118,107 @@ public async Task HandleEmailNotificationAsync(ITurnContext turnContext, ITurnSt
117118 await turnContext . SendActivityAsync ( responseActivity ) ;
118119 }
119120
120- public Task HandleCommentNotificationAsync ( ITurnContext turnContext , ITurnState turnState , AgentNotificationActivity commentEvent )
121+ public async Task HandleCommentNotificationAsync ( ITurnContext turnContext , ITurnState turnState , AgentNotificationActivity commentEvent )
121122 {
122123 _logger . LogInformation ( "Processing comment notification (Responses API)" ) ;
123- return Task . CompletedTask ;
124+
125+ var comment = commentEvent . WpxCommentNotification ;
126+ if ( comment == null )
127+ {
128+ _logger . LogWarning ( "Comment notification received without WpxComment payload; skipping." ) ;
129+ return ;
130+ }
131+
132+ // The document the comment lives on is delivered as the first attachment on the activity.
133+ var attachments = turnContext . Activity . Attachments ;
134+ var contentUrl = attachments ? . FirstOrDefault ( ) ? . ContentUrl ;
135+ if ( string . IsNullOrEmpty ( contentUrl ) )
136+ {
137+ _logger . LogWarning (
138+ "Comment notification for CommentId={CommentId} on DocumentId={DocumentId} has no attachment ContentUrl; cannot fetch document content." ,
139+ comment . CommentId ,
140+ comment . DocumentId ) ;
141+ return ;
142+ }
143+
144+ // Figure out which Office product (and therefore which MCP server) to use.
145+ // The sub-channel is set by the OnAgenticWord/Excel/PowerPointNotification routers.
146+ var subChannel = turnContext . Activity . ChannelId ? . SubChannel ?? string . Empty ;
147+ string productLabel ;
148+ string mcpServerName ;
149+ if ( subChannel . Equals ( SubChannels . AgentsWordSubChannel , StringComparison . OrdinalIgnoreCase ) )
150+ {
151+ productLabel = "Word" ;
152+ mcpServerName = "mcp_WordServer" ;
153+ }
154+ else if ( subChannel . Equals ( SubChannels . AgentsExcelSubChannel , StringComparison . OrdinalIgnoreCase ) )
155+ {
156+ productLabel = "Excel" ;
157+ mcpServerName = "mcp_ExcelServer" ;
158+ }
159+ else if ( subChannel . Equals ( SubChannels . AgentsPowerPointSubChannel , StringComparison . OrdinalIgnoreCase ) )
160+ {
161+ productLabel = "PowerPoint" ;
162+ mcpServerName = "mcp_PowerPointServer" ;
163+ }
164+ else
165+ {
166+ // Fall back to inferring from the file extension on the content URL.
167+ ( productLabel , mcpServerName ) = InferProductFromUrl ( contentUrl ) ;
168+ }
169+
170+ var commenter = commentEvent . From ? . Name ?? commentEvent . From ? . Id ?? "the commenter" ;
171+ var commentText = ( turnContext . Activity . Text ?? string . Empty ) . Trim ( ) ;
172+ var commentSnippet = string . IsNullOrEmpty ( commentText ) ? "(no comment text)" : commentText ;
173+ var conversationId = $ "comment:{ comment . DocumentId ?? "unknown-doc" } :{ comment . CommentId ?? "unknown-comment" } ";
174+
175+ var prompt = $ """
176+ You have been @-mentioned in a { productLabel } comment and must reply to it.
177+
178+ Use the { mcpServerName } MCP tools to do the following, in order:
179+ 1. Call GetDocumentContent with the sharing URL below to read the document and
180+ locate the text that the comment refers to.
181+ 2. Call ReplyToComment with commentId="{ comment . CommentId } " to post your reply
182+ directly on the thread. Do NOT respond via chat or email — the reply must be
183+ posted through the { mcpServerName } ReplyToComment tool so it shows up on the
184+ comment thread in the document.
185+
186+ Keep the reply concise, helpful, and grounded in the actual document content.
187+ Format the reply as plain text (the comment thread does not render HTML).
188+
189+ Document URL: { contentUrl }
190+ DocumentId: { comment . DocumentId }
191+ CommentId: { comment . CommentId }
192+ ParentCommentId: { comment . ParentCommentId ?? "(none — this is a top-level comment)" }
193+ Commenter: { commenter }
194+ Comment text: { commentSnippet }
195+ """ ;
196+
197+ var response = await InvokeResponsesApiAsync ( prompt , conversationId ) ;
198+
199+ // The reply is posted on the comment thread by the MCP server's ReplyToComment tool,
200+ // so there is nothing to send back through the activity protocol here. The model's
201+ // final text (if any) is logged for diagnostics only.
202+ _logger . LogInformation (
203+ "Comment reply flow finished for {Product} CommentId={CommentId}. Model output (for diagnostics only): {Response}" ,
204+ productLabel ,
205+ comment . CommentId ,
206+ string . IsNullOrWhiteSpace ( response ) ? "(empty — reply was posted via MCP tool)" : response ) ;
207+ }
208+
209+ private static ( string Product , string McpServer ) InferProductFromUrl ( string url )
210+ {
211+ var lower = url . ToLowerInvariant ( ) ;
212+ if ( lower . Contains ( ".xlsx" ) || lower . Contains ( ".xlsm" ) || lower . Contains ( ".xlsb" ) )
213+ {
214+ return ( "Excel" , "mcp_ExcelServer" ) ;
215+ }
216+ if ( lower . Contains ( ".pptx" ) || lower . Contains ( ".ppt" ) )
217+ {
218+ return ( "PowerPoint" , "mcp_PowerPointServer" ) ;
219+ }
220+ // Default to Word — covers .docx/.doc and the unknown case.
221+ return ( "Word" , "mcp_WordServer" ) ;
124222 }
125223
126224 public Task HandleInstallationUpdateAsync ( ITurnContext turnContext , ITurnState turnState , AgentNotificationActivity installationEvent )
0 commit comments