feat: Add SharePoint Knowledge Agent - AI Agent for Document-Based Knowledge Management#30
Conversation
- C# .NET 8 AI Agent that reads documents from SharePoint via Microsoft Graph API - Creates Knowledge Articles from document content with text extraction and chunking - Stores document embeddings in Qdrant vector database for semantic search - Integrates with Azure Application Insights to detect issues/exceptions - Retrieves relevant Knowledge Articles when alerts are detected - Sends email notifications with alert details and related knowledge articles - Background jobs for periodic SharePoint sync and alert monitoring - Webhook endpoint for real-time Application Insights alert processing - Docker and docker-compose support for containerized deployment - Clean Architecture: Core (domain), Infrastructure (services), API (endpoints)
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| _ = Task.Run(async () => | ||
| { | ||
| try | ||
| { | ||
| await _orchestrator.SyncSharePointDocumentsAsync(cancellationToken); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Error during manual sync"); | ||
| } | ||
| }, cancellationToken); |
There was a problem hiding this comment.
🔴 Request-scoped CancellationToken passed to fire-and-forget Task.Run cancels background work
In TriggerSync and TriggerMonitoring, the cancellationToken parameter is bound to the HTTP request's HttpContext.RequestAborted. Since the controller returns Accepted() immediately, the HTTP connection closes shortly after, and ASP.NET Core signals the cancellation token. The background Task.Run receives this token both as its scheduler token (line 40/61) and passes it into the orchestrator methods (line 34/55), causing the sync/monitoring work to be cancelled almost immediately via OperationCanceledException.
Prompt for agents
In AgentController.TriggerSync (and similarly TriggerMonitoring), the CancellationToken from the HTTP request is passed into Task.Run and to the orchestrator methods. This token gets cancelled when the HTTP response is sent and the connection closes, aborting the background work.
The fix should use CancellationToken.None (or an application-lifetime token) for the background work instead of the request-scoped cancellationToken. You could inject IHostApplicationLifetime and use its ApplicationStopping token, or simply use CancellationToken.None.
Alternatively, this fire-and-forget pattern should be replaced with a proper background task queue (e.g., IBackgroundTaskQueue or Channel<T>) to avoid additional issues with using scoped services after the request scope is disposed.
Affected methods: TriggerSync (lines 25-43), TriggerMonitoring (lines 47-64) in AgentController.cs.
Was this helpful? React with 👍 or 👎 to provide feedback.
| private readonly EmailOptions _emailOptions; | ||
| private readonly ILogger<KnowledgeAgentOrchestrator> _logger; | ||
|
|
||
| private DateTime _lastSyncTime = DateTime.MinValue; |
There was a problem hiding this comment.
🔴 Incremental sync never works: _lastSyncTime resets on every scope because orchestrator is scoped
_lastSyncTime is an instance field on KnowledgeAgentOrchestrator (line 19), but the orchestrator is registered as scoped in DependencyInjection.cs:30. Every time SharePointSyncJob runs its loop iteration, it creates a new DI scope (SharePointSyncJob.cs:38), which creates a new KnowledgeAgentOrchestrator instance with _lastSyncTime = DateTime.MinValue. The conditional at line 47 therefore always takes the full sync branch — the incremental sync path (fetching only modified documents) is dead code.
Prompt for agents
The _lastSyncTime instance field in KnowledgeAgentOrchestrator is meant to track the last successful sync time to enable incremental syncing (only fetching modified documents). However, KnowledgeAgentOrchestrator is registered as scoped (DependencyInjection.cs:30), so each scope creates a fresh instance where _lastSyncTime is DateTime.MinValue.
The SharePointSyncJob creates a new scope on every loop iteration (SharePointSyncJob.cs:38), so each sync always does a full document fetch instead of an incremental one.
Possible fixes:
1. Make _lastSyncTime a static field so it persists across instances.
2. Extract sync state into a dedicated singleton service that tracks the last sync time.
3. Change the orchestrator registration to singleton (but this would conflict with its scoped dependencies).
Option 2 is the cleanest approach — create an ISyncStateService singleton that stores the last sync timestamp.
Was this helpful? React with 👍 or 👎 to provide feedback.
| var response = await QueryAppInsightsAsync(query, cancellationToken); | ||
|
|
||
| if (response != null) | ||
| { | ||
| alerts.AddRange(ParseExceptionResults(response)); |
There was a problem hiding this comment.
🟡 JsonDocument returned from QueryAppInsightsAsync is never disposed, causing resource leak
QueryAppInsightsAsync returns a JsonDocument at AppInsightsService.cs:98, which implements IDisposable and holds pooled memory. In GetRecentAlertsAsync (line 45-49), the returned JsonDocument is used but never disposed. Over repeated monitoring cycles, this leaks memory from the ArrayPool<byte> used internally by JsonDocument.
| var response = await QueryAppInsightsAsync(query, cancellationToken); | |
| if (response != null) | |
| { | |
| alerts.AddRange(ParseExceptionResults(response)); | |
| var response = await QueryAppInsightsAsync(query, cancellationToken); | |
| if (response != null) | |
| { | |
| using (response) | |
| { | |
| alerts.AddRange(ParseExceptionResults(response)); | |
| } | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Adds a new SharePoint Knowledge Agent microservice built with C# .NET 8 that:
Architecture
/api/agent/alerts/webhookKey Components
SharePointServiceDocumentProcessorEmbeddingServiceQdrantVectorStoreServiceKnowledgeArticleServiceAppInsightsServiceEmailNotificationServiceKnowledgeAgentOrchestratorAPI Endpoints
POST /api/knowledge/search— Semantic search across knowledge articlesGET /api/knowledge/articles— List all knowledge articlesPOST /api/agent/sync— Trigger manual SharePoint syncPOST /api/agent/monitor— Trigger manual monitoring cyclePOST /api/agent/alerts/webhook— Application Insights alert webhookGET /health— Health checkReview & Testing Checklist for Human
Sites.Read.AllandFiles.Read.AllGraph API permissions configuredappsettings.jsonis updated with actual SharePoint site/drive IDs, Azure OpenAI keys, App Insights connection string, and SMTP credentials before deploymentNotes
global.jsonpins the SDK versionLink to Devin session: https://partner-workshops.devinenterprise.com/sessions/7c2aa9ed4a76462ab9440fee8dce82bd