From b4009327eed901419f52962072a7a5392b18538b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:57:02 +0000 Subject: [PATCH] feat: Add SharePoint Knowledge Agent AI service - 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) --- src/Services/KnowledgeAgent/Dockerfile | 21 ++ .../Controllers/AgentController.cs | 96 +++++++ .../Controllers/HealthController.cs | 21 ++ .../Controllers/KnowledgeController.cs | 59 +++++ .../DTOs/AlertWebhookRequest.cs | 14 ++ .../KnowledgeAgent.API/DTOs/SearchRequest.cs | 7 + .../KnowledgeAgent.API/DTOs/SyncRequest.cs | 7 + .../KnowledgeAgent.API.csproj | 16 ++ .../KnowledgeAgent.API/Program.cs | 58 +++++ .../appsettings.Development.json | 16 ++ .../KnowledgeAgent.API/appsettings.json | 54 ++++ .../Enums/ArticleStatus.cs | 10 + .../Interfaces/IAppInsightsService.cs | 11 + .../Interfaces/IDocumentProcessor.cs | 9 + .../Interfaces/IEmailNotificationService.cs | 9 + .../Interfaces/IEmbeddingService.cs | 7 + .../Interfaces/IKnowledgeAgentOrchestrator.cs | 10 + .../Interfaces/IKnowledgeArticleService.cs | 12 + .../Interfaces/ISharePointService.cs | 11 + .../Interfaces/IVectorStoreService.cs | 11 + .../KnowledgeAgent.Core.csproj | 7 + .../Models/AppInsightAlert.cs | 15 ++ .../Models/DocumentChunk.cs | 12 + .../Models/EmailNotification.cs | 20 ++ .../Models/KnowledgeArticle.cs | 17 ++ .../Models/SearchResult.cs | 10 + .../Models/SharePointDocument.cs | 14 ++ .../BackgroundJobs/AlertMonitoringJob.cs | 51 ++++ .../BackgroundJobs/SharePointSyncJob.cs | 51 ++++ .../Configuration/AppInsightsOptions.cs | 13 + .../Configuration/EmailOptions.cs | 15 ++ .../Configuration/OpenAIOptions.cs | 13 + .../Configuration/SharePointOptions.cs | 14 ++ .../Configuration/VectorStoreOptions.cs | 12 + .../DependencyInjection.cs | 41 +++ .../KnowledgeAgent.Infrastructure.csproj | 22 ++ .../Services/AppInsightsService.cs | 137 ++++++++++ .../Services/DocumentProcessor.cs | 236 ++++++++++++++++++ .../Services/EmailNotificationService.cs | 187 ++++++++++++++ .../Services/EmbeddingService.cs | 77 ++++++ .../Services/KnowledgeAgentOrchestrator.cs | 193 ++++++++++++++ .../Services/KnowledgeArticleService.cs | 149 +++++++++++ .../Services/QdrantVectorStoreService.cs | 163 ++++++++++++ .../Services/SharePointService.cs | 154 ++++++++++++ .../KnowledgeAgent/KnowledgeAgent.sln | 31 +++ src/Services/KnowledgeAgent/README.md | 200 +++++++++++++++ .../KnowledgeAgent/docker-compose.yml | 48 ++++ src/Services/KnowledgeAgent/global.json | 6 + 48 files changed, 2367 insertions(+) create mode 100644 src/Services/KnowledgeAgent/Dockerfile create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.API/Controllers/AgentController.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.API/Controllers/HealthController.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.API/Controllers/KnowledgeController.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.API/DTOs/AlertWebhookRequest.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.API/DTOs/SearchRequest.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.API/DTOs/SyncRequest.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.API/KnowledgeAgent.API.csproj create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.API/Program.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.API/appsettings.Development.json create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.API/appsettings.json create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Enums/ArticleStatus.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IAppInsightsService.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IDocumentProcessor.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IEmailNotificationService.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IEmbeddingService.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IKnowledgeAgentOrchestrator.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IKnowledgeArticleService.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/ISharePointService.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IVectorStoreService.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/KnowledgeAgent.Core.csproj create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/AppInsightAlert.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/DocumentChunk.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/EmailNotification.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/KnowledgeArticle.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/SearchResult.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/SharePointDocument.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/BackgroundJobs/AlertMonitoringJob.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/BackgroundJobs/SharePointSyncJob.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/AppInsightsOptions.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/EmailOptions.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/OpenAIOptions.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/SharePointOptions.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/VectorStoreOptions.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/DependencyInjection.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/KnowledgeAgent.Infrastructure.csproj create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/AppInsightsService.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/DocumentProcessor.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/EmailNotificationService.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/EmbeddingService.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/KnowledgeAgentOrchestrator.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/KnowledgeArticleService.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/QdrantVectorStoreService.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/SharePointService.cs create mode 100644 src/Services/KnowledgeAgent/KnowledgeAgent.sln create mode 100644 src/Services/KnowledgeAgent/README.md create mode 100644 src/Services/KnowledgeAgent/docker-compose.yml create mode 100644 src/Services/KnowledgeAgent/global.json diff --git a/src/Services/KnowledgeAgent/Dockerfile b/src/Services/KnowledgeAgent/Dockerfile new file mode 100644 index 0000000..4062306 --- /dev/null +++ b/src/Services/KnowledgeAgent/Dockerfile @@ -0,0 +1,21 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 8080 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY ["KnowledgeAgent.API/KnowledgeAgent.API.csproj", "KnowledgeAgent.API/"] +COPY ["KnowledgeAgent.Core/KnowledgeAgent.Core.csproj", "KnowledgeAgent.Core/"] +COPY ["KnowledgeAgent.Infrastructure/KnowledgeAgent.Infrastructure.csproj", "KnowledgeAgent.Infrastructure/"] +RUN dotnet restore "KnowledgeAgent.API/KnowledgeAgent.API.csproj" +COPY . . +WORKDIR "/src/KnowledgeAgent.API" +RUN dotnet build "KnowledgeAgent.API.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "KnowledgeAgent.API.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "KnowledgeAgent.API.dll"] diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.API/Controllers/AgentController.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.API/Controllers/AgentController.cs new file mode 100644 index 0000000..519ea1f --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.API/Controllers/AgentController.cs @@ -0,0 +1,96 @@ +using KnowledgeAgent.API.DTOs; +using KnowledgeAgent.Core.Interfaces; +using KnowledgeAgent.Core.Models; +using Microsoft.AspNetCore.Mvc; + +namespace KnowledgeAgent.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AgentController : ControllerBase +{ + private readonly IKnowledgeAgentOrchestrator _orchestrator; + private readonly ILogger _logger; + + public AgentController( + IKnowledgeAgentOrchestrator orchestrator, + ILogger logger) + { + _orchestrator = orchestrator; + _logger = logger; + } + + [HttpPost("sync")] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public Task TriggerSync(CancellationToken cancellationToken) + { + _logger.LogInformation("Manual SharePoint sync triggered"); + + // Run sync in background, return immediately + _ = Task.Run(async () => + { + try + { + await _orchestrator.SyncSharePointDocumentsAsync(cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during manual sync"); + } + }, cancellationToken); + + return Task.FromResult(Accepted(new { message = "SharePoint document sync initiated" })); + } + + [HttpPost("monitor")] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public Task TriggerMonitoring(CancellationToken cancellationToken) + { + _logger.LogInformation("Manual monitoring cycle triggered"); + + _ = Task.Run(async () => + { + try + { + await _orchestrator.RunMonitoringCycleAsync(cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during manual monitoring cycle"); + } + }, cancellationToken); + + return Task.FromResult(Accepted(new { message = "Monitoring cycle initiated" })); + } + + [HttpPost("alerts/webhook")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task HandleAlertWebhook( + [FromBody] AlertWebhookRequest request, CancellationToken cancellationToken) + { + _logger.LogInformation("Received alert webhook: {AlertId} - {AlertName}", + request.AlertId, request.AlertName); + + if (string.IsNullOrWhiteSpace(request.AlertId)) + return BadRequest("AlertId is required"); + + var alert = new AppInsightAlert + { + AlertId = request.AlertId, + AlertName = request.AlertName, + Severity = request.Severity, + Description = request.Description, + AffectedResource = request.AffectedResource, + ExceptionType = request.ExceptionType, + ExceptionMessage = request.ExceptionMessage, + StackTrace = request.StackTrace, + FiredAt = DateTime.UtcNow, + CustomProperties = request.CustomProperties ?? new Dictionary() + }; + + await _orchestrator.ProcessAlertAsync(alert, cancellationToken); + + return Ok(new { message = "Alert processed successfully", alertId = alert.AlertId }); + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.API/Controllers/HealthController.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.API/Controllers/HealthController.cs new file mode 100644 index 0000000..de22d93 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.API/Controllers/HealthController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; + +namespace KnowledgeAgent.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class HealthController : ControllerBase +{ + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult GetHealth() + { + return Ok(new + { + status = "Healthy", + service = "SharePoint Knowledge Agent", + version = "1.0.0", + timestamp = DateTime.UtcNow + }); + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.API/Controllers/KnowledgeController.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.API/Controllers/KnowledgeController.cs new file mode 100644 index 0000000..a96d81c --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.API/Controllers/KnowledgeController.cs @@ -0,0 +1,59 @@ +using KnowledgeAgent.API.DTOs; +using KnowledgeAgent.Core.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace KnowledgeAgent.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class KnowledgeController : ControllerBase +{ + private readonly IKnowledgeArticleService _knowledgeArticleService; + private readonly ILogger _logger; + + public KnowledgeController( + IKnowledgeArticleService knowledgeArticleService, + ILogger logger) + { + _knowledgeArticleService = knowledgeArticleService; + _logger = logger; + } + + [HttpPost("search")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task SearchKnowledge( + [FromBody] SearchRequest request, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(request.Query)) + return BadRequest("Query cannot be empty"); + + _logger.LogInformation("Knowledge search request: {Query}", request.Query); + + var results = await _knowledgeArticleService.SearchKnowledgeAsync( + request.Query, request.TopK, cancellationToken); + + return Ok(results); + } + + [HttpGet("articles")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GetAllArticles(CancellationToken cancellationToken) + { + var articles = await _knowledgeArticleService.GetAllAsync(cancellationToken); + return Ok(articles); + } + + [HttpGet("articles/{id:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetArticle(Guid id, CancellationToken cancellationToken) + { + var article = await _knowledgeArticleService.GetByIdAsync(id, cancellationToken); + + if (article == null) + return NotFound(); + + return Ok(article); + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.API/DTOs/AlertWebhookRequest.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.API/DTOs/AlertWebhookRequest.cs new file mode 100644 index 0000000..d3ebf28 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.API/DTOs/AlertWebhookRequest.cs @@ -0,0 +1,14 @@ +namespace KnowledgeAgent.API.DTOs; + +public class AlertWebhookRequest +{ + public string AlertId { get; set; } = string.Empty; + public string AlertName { get; set; } = string.Empty; + public string Severity { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string AffectedResource { get; set; } = string.Empty; + public string ExceptionType { get; set; } = string.Empty; + public string ExceptionMessage { get; set; } = string.Empty; + public string StackTrace { get; set; } = string.Empty; + public Dictionary? CustomProperties { get; set; } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.API/DTOs/SearchRequest.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.API/DTOs/SearchRequest.cs new file mode 100644 index 0000000..f2549b4 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.API/DTOs/SearchRequest.cs @@ -0,0 +1,7 @@ +namespace KnowledgeAgent.API.DTOs; + +public class SearchRequest +{ + public string Query { get; set; } = string.Empty; + public int TopK { get; set; } = 5; +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.API/DTOs/SyncRequest.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.API/DTOs/SyncRequest.cs new file mode 100644 index 0000000..4eee841 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.API/DTOs/SyncRequest.cs @@ -0,0 +1,7 @@ +namespace KnowledgeAgent.API.DTOs; + +public class SyncRequest +{ + public string? SiteId { get; set; } + public string? DriveId { get; set; } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.API/KnowledgeAgent.API.csproj b/src/Services/KnowledgeAgent/KnowledgeAgent.API/KnowledgeAgent.API.csproj new file mode 100644 index 0000000..4490625 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.API/KnowledgeAgent.API.csproj @@ -0,0 +1,16 @@ + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.API/Program.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.API/Program.cs new file mode 100644 index 0000000..baef619 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.API/Program.cs @@ -0,0 +1,58 @@ +using KnowledgeAgent.Core.Interfaces; +using KnowledgeAgent.Infrastructure; + +var builder = WebApplication.CreateBuilder(args); + +// Add Application Insights telemetry +builder.Services.AddApplicationInsightsTelemetry(options => +{ + options.ConnectionString = builder.Configuration["AppInsights:ConnectionString"]; +}); + +// Add Knowledge Agent infrastructure services +builder.Services.AddKnowledgeAgentInfrastructure(builder.Configuration); + +// Add controllers and Swagger +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo + { + Title = "SharePoint Knowledge Agent API", + Version = "v1", + Description = "AI Agent that reads SharePoint documents, creates Knowledge Articles, " + + "stores them in a vector database, and integrates with Application Insights " + + "to provide relevant knowledge when issues are detected." + }); +}); + +// Add health checks +builder.Services.AddHealthChecks(); + +var app = builder.Build(); + +// Initialize vector store on startup +using (var scope = app.Services.CreateScope()) +{ + var vectorStore = scope.ServiceProvider.GetRequiredService(); + await vectorStore.InitializeAsync(); +} + +// Configure middleware +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "Knowledge Agent API v1"); + c.RoutePrefix = string.Empty; + }); +} + +app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); +app.MapHealthChecks("/health"); + +app.Run(); diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.API/appsettings.Development.json b/src/Services/KnowledgeAgent/KnowledgeAgent.API/appsettings.Development.json new file mode 100644 index 0000000..b30465f --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.API/appsettings.Development.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Information", + "KnowledgeAgent": "Trace" + } + }, + "SharePoint": { + "SyncIntervalMinutes": 5 + }, + "AppInsights": { + "MonitoringIntervalMinutes": 2, + "AlertLookbackMinutes": 30 + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.API/appsettings.json b/src/Services/KnowledgeAgent/KnowledgeAgent.API/appsettings.json new file mode 100644 index 0000000..8e62652 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.API/appsettings.json @@ -0,0 +1,54 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "KnowledgeAgent": "Debug" + } + }, + "AllowedHosts": "*", + "SharePoint": { + "TenantId": "", + "ClientId": "", + "ClientSecret": "", + "SiteId": "", + "DriveId": "", + "FolderPath": "/Documents/KnowledgeBase", + "SyncIntervalMinutes": 60 + }, + "VectorStore": { + "Host": "localhost", + "Port": 6334, + "CollectionName": "knowledge_articles", + "VectorSize": 1536, + "ApiKey": "" + }, + "OpenAI": { + "ApiKey": "", + "Endpoint": "https://.openai.azure.com/", + "EmbeddingModel": "text-embedding-ada-002", + "CompletionModel": "gpt-4", + "UseAzureOpenAI": true, + "AzureDeploymentName": "" + }, + "AppInsights": { + "ConnectionString": "", + "InstrumentationKey": "", + "ApplicationId": "", + "ApiKey": "", + "MonitoringIntervalMinutes": 5, + "AlertLookbackMinutes": 15 + }, + "Email": { + "SmtpHost": "smtp.office365.com", + "SmtpPort": 587, + "SmtpUsername": "", + "SmtpPassword": "", + "UseSsl": true, + "FromAddress": "knowledge-agent@yourdomain.com", + "FromName": "Knowledge Agent", + "DefaultRecipients": [ + "team@yourdomain.com" + ] + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Enums/ArticleStatus.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Enums/ArticleStatus.cs new file mode 100644 index 0000000..f0d6271 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Enums/ArticleStatus.cs @@ -0,0 +1,10 @@ +namespace KnowledgeAgent.Core.Models; + +public enum ArticleStatus +{ + Draft, + Processing, + Indexed, + Failed, + Archived +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IAppInsightsService.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IAppInsightsService.cs new file mode 100644 index 0000000..426b089 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IAppInsightsService.cs @@ -0,0 +1,11 @@ +using KnowledgeAgent.Core.Models; + +namespace KnowledgeAgent.Core.Interfaces; + +public interface IAppInsightsService +{ + Task> GetRecentAlertsAsync(TimeSpan lookbackPeriod, CancellationToken cancellationToken = default); + Task> GetActiveExceptionsAsync(CancellationToken cancellationToken = default); + void TrackEvent(string eventName, Dictionary? properties = null); + void TrackException(Exception exception, Dictionary? properties = null); +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IDocumentProcessor.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IDocumentProcessor.cs new file mode 100644 index 0000000..2086fb5 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IDocumentProcessor.cs @@ -0,0 +1,9 @@ +using KnowledgeAgent.Core.Models; + +namespace KnowledgeAgent.Core.Interfaces; + +public interface IDocumentProcessor +{ + Task ExtractTextAsync(Stream documentStream, string contentType, CancellationToken cancellationToken = default); + IEnumerable ChunkDocument(string text, Guid knowledgeArticleId, int maxChunkSize = 1000, int overlapSize = 200); +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IEmailNotificationService.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IEmailNotificationService.cs new file mode 100644 index 0000000..48a7967 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IEmailNotificationService.cs @@ -0,0 +1,9 @@ +using KnowledgeAgent.Core.Models; + +namespace KnowledgeAgent.Core.Interfaces; + +public interface IEmailNotificationService +{ + Task SendAlertWithKnowledgeAsync(AppInsightAlert alert, IEnumerable relevantArticles, IEnumerable recipients, CancellationToken cancellationToken = default); + Task SendDigestEmailAsync(IEnumerable alerts, IEnumerable recipients, CancellationToken cancellationToken = default); +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IEmbeddingService.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IEmbeddingService.cs new file mode 100644 index 0000000..df1ada0 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IEmbeddingService.cs @@ -0,0 +1,7 @@ +namespace KnowledgeAgent.Core.Interfaces; + +public interface IEmbeddingService +{ + Task GenerateEmbeddingAsync(string text, CancellationToken cancellationToken = default); + Task> GenerateEmbeddingsAsync(IEnumerable texts, CancellationToken cancellationToken = default); +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IKnowledgeAgentOrchestrator.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IKnowledgeAgentOrchestrator.cs new file mode 100644 index 0000000..a4088c5 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IKnowledgeAgentOrchestrator.cs @@ -0,0 +1,10 @@ +using KnowledgeAgent.Core.Models; + +namespace KnowledgeAgent.Core.Interfaces; + +public interface IKnowledgeAgentOrchestrator +{ + Task SyncSharePointDocumentsAsync(CancellationToken cancellationToken = default); + Task ProcessAlertAsync(AppInsightAlert alert, CancellationToken cancellationToken = default); + Task RunMonitoringCycleAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IKnowledgeArticleService.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IKnowledgeArticleService.cs new file mode 100644 index 0000000..d0845a2 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IKnowledgeArticleService.cs @@ -0,0 +1,12 @@ +using KnowledgeAgent.Core.Models; + +namespace KnowledgeAgent.Core.Interfaces; + +public interface IKnowledgeArticleService +{ + Task CreateFromDocumentAsync(SharePointDocument document, Stream content, CancellationToken cancellationToken = default); + Task> SearchKnowledgeAsync(string query, int topK = 5, CancellationToken cancellationToken = default); + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task ProcessAndIndexDocumentAsync(SharePointDocument document, Stream content, CancellationToken cancellationToken = default); +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/ISharePointService.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/ISharePointService.cs new file mode 100644 index 0000000..28ab7ce --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/ISharePointService.cs @@ -0,0 +1,11 @@ +using KnowledgeAgent.Core.Models; + +namespace KnowledgeAgent.Core.Interfaces; + +public interface ISharePointService +{ + Task> GetDocumentsAsync(string siteId, string driveId, CancellationToken cancellationToken = default); + Task DownloadDocumentAsync(string driveId, string itemId, CancellationToken cancellationToken = default); + Task GetDocumentMetadataAsync(string driveId, string itemId, CancellationToken cancellationToken = default); + Task> GetModifiedDocumentsAsync(string siteId, string driveId, DateTime since, CancellationToken cancellationToken = default); +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IVectorStoreService.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IVectorStoreService.cs new file mode 100644 index 0000000..9bcc914 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Interfaces/IVectorStoreService.cs @@ -0,0 +1,11 @@ +using KnowledgeAgent.Core.Models; + +namespace KnowledgeAgent.Core.Interfaces; + +public interface IVectorStoreService +{ + Task InitializeAsync(CancellationToken cancellationToken = default); + Task UpsertChunksAsync(IEnumerable chunks, CancellationToken cancellationToken = default); + Task> SearchAsync(float[] queryEmbedding, int topK = 5, float scoreThreshold = 0.7f, CancellationToken cancellationToken = default); + Task DeleteByArticleIdAsync(Guid knowledgeArticleId, CancellationToken cancellationToken = default); +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/KnowledgeAgent.Core.csproj b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/KnowledgeAgent.Core.csproj new file mode 100644 index 0000000..bfa77a1 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/KnowledgeAgent.Core.csproj @@ -0,0 +1,7 @@ + + + net8.0 + enable + enable + + diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/AppInsightAlert.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/AppInsightAlert.cs new file mode 100644 index 0000000..87bfc83 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/AppInsightAlert.cs @@ -0,0 +1,15 @@ +namespace KnowledgeAgent.Core.Models; + +public class AppInsightAlert +{ + public string AlertId { get; set; } = string.Empty; + public string AlertName { get; set; } = string.Empty; + public string Severity { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string AffectedResource { get; set; } = string.Empty; + public DateTime FiredAt { get; set; } + public string ExceptionType { get; set; } = string.Empty; + public string ExceptionMessage { get; set; } = string.Empty; + public string StackTrace { get; set; } = string.Empty; + public Dictionary CustomProperties { get; set; } = new(); +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/DocumentChunk.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/DocumentChunk.cs new file mode 100644 index 0000000..25eef53 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/DocumentChunk.cs @@ -0,0 +1,12 @@ +namespace KnowledgeAgent.Core.Models; + +public class DocumentChunk +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public Guid KnowledgeArticleId { get; set; } + public string Content { get; set; } = string.Empty; + public int ChunkIndex { get; set; } + public int TokenCount { get; set; } + public float[] Embedding { get; set; } = Array.Empty(); + public Dictionary Metadata { get; set; } = new(); +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/EmailNotification.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/EmailNotification.cs new file mode 100644 index 0000000..71d3a90 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/EmailNotification.cs @@ -0,0 +1,20 @@ +namespace KnowledgeAgent.Core.Models; + +public class EmailNotification +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public string Subject { get; set; } = string.Empty; + public string Body { get; set; } = string.Empty; + public List Recipients { get; set; } = new(); + public string AlertId { get; set; } = string.Empty; + public List ReferencedArticleIds { get; set; } = new(); + public DateTime SentAt { get; set; } + public NotificationStatus Status { get; set; } = NotificationStatus.Pending; +} + +public enum NotificationStatus +{ + Pending, + Sent, + Failed +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/KnowledgeArticle.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/KnowledgeArticle.cs new file mode 100644 index 0000000..156de77 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/KnowledgeArticle.cs @@ -0,0 +1,17 @@ +namespace KnowledgeAgent.Core.Models; + +public class KnowledgeArticle +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public string Title { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + public string SourceDocumentId { get; set; } = string.Empty; + public string SourceDocumentName { get; set; } = string.Empty; + public string SharePointSiteId { get; set; } = string.Empty; + public string SharePointDriveId { get; set; } = string.Empty; + public List Tags { get; set; } = new(); + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public ArticleStatus Status { get; set; } = ArticleStatus.Draft; +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/SearchResult.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/SearchResult.cs new file mode 100644 index 0000000..3bbcf71 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/SearchResult.cs @@ -0,0 +1,10 @@ +namespace KnowledgeAgent.Core.Models; + +public class SearchResult +{ + public Guid KnowledgeArticleId { get; set; } + public string Title { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public float Score { get; set; } + public Dictionary Metadata { get; set; } = new(); +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/SharePointDocument.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/SharePointDocument.cs new file mode 100644 index 0000000..9e42103 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Core/Models/SharePointDocument.cs @@ -0,0 +1,14 @@ +namespace KnowledgeAgent.Core.Models; + +public class SharePointDocument +{ + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string ContentType { get; set; } = string.Empty; + public long Size { get; set; } + public string WebUrl { get; set; } = string.Empty; + public string DriveId { get; set; } = string.Empty; + public string SiteId { get; set; } = string.Empty; + public DateTime LastModified { get; set; } + public string LastModifiedBy { get; set; } = string.Empty; +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/BackgroundJobs/AlertMonitoringJob.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/BackgroundJobs/AlertMonitoringJob.cs new file mode 100644 index 0000000..9c46e70 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/BackgroundJobs/AlertMonitoringJob.cs @@ -0,0 +1,51 @@ +using KnowledgeAgent.Core.Interfaces; +using KnowledgeAgent.Infrastructure.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace KnowledgeAgent.Infrastructure.BackgroundJobs; + +public class AlertMonitoringJob : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly AppInsightsOptions _options; + private readonly ILogger _logger; + + public AlertMonitoringJob( + IServiceScopeFactory scopeFactory, + IOptions options, + ILogger logger) + { + _scopeFactory = scopeFactory; + _options = options.Value; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Alert monitoring job started. Interval: {Interval} minutes", + _options.MonitoringIntervalMinutes); + + // Initial delay to let the application start up + await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + using var scope = _scopeFactory.CreateScope(); + var orchestrator = scope.ServiceProvider.GetRequiredService(); + + await orchestrator.RunMonitoringCycleAsync(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in alert monitoring job"); + } + + await Task.Delay(TimeSpan.FromMinutes(_options.MonitoringIntervalMinutes), stoppingToken); + } + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/BackgroundJobs/SharePointSyncJob.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/BackgroundJobs/SharePointSyncJob.cs new file mode 100644 index 0000000..58c20f8 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/BackgroundJobs/SharePointSyncJob.cs @@ -0,0 +1,51 @@ +using KnowledgeAgent.Core.Interfaces; +using KnowledgeAgent.Infrastructure.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace KnowledgeAgent.Infrastructure.BackgroundJobs; + +public class SharePointSyncJob : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly SharePointOptions _options; + private readonly ILogger _logger; + + public SharePointSyncJob( + IServiceScopeFactory scopeFactory, + IOptions options, + ILogger logger) + { + _scopeFactory = scopeFactory; + _options = options.Value; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("SharePoint sync job started. Interval: {Interval} minutes", + _options.SyncIntervalMinutes); + + // Initial delay to let the application start up + await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + using var scope = _scopeFactory.CreateScope(); + var orchestrator = scope.ServiceProvider.GetRequiredService(); + + await orchestrator.SyncSharePointDocumentsAsync(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in SharePoint sync job"); + } + + await Task.Delay(TimeSpan.FromMinutes(_options.SyncIntervalMinutes), stoppingToken); + } + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/AppInsightsOptions.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/AppInsightsOptions.cs new file mode 100644 index 0000000..e6916cb --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/AppInsightsOptions.cs @@ -0,0 +1,13 @@ +namespace KnowledgeAgent.Infrastructure.Configuration; + +public class AppInsightsOptions +{ + public const string SectionName = "AppInsights"; + + public string ConnectionString { get; set; } = string.Empty; + public string InstrumentationKey { get; set; } = string.Empty; + public string ApplicationId { get; set; } = string.Empty; + public string ApiKey { get; set; } = string.Empty; + public int MonitoringIntervalMinutes { get; set; } = 5; + public int AlertLookbackMinutes { get; set; } = 15; +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/EmailOptions.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/EmailOptions.cs new file mode 100644 index 0000000..6352c71 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/EmailOptions.cs @@ -0,0 +1,15 @@ +namespace KnowledgeAgent.Infrastructure.Configuration; + +public class EmailOptions +{ + public const string SectionName = "Email"; + + public string SmtpHost { get; set; } = string.Empty; + public int SmtpPort { get; set; } = 587; + public string SmtpUsername { get; set; } = string.Empty; + public string SmtpPassword { get; set; } = string.Empty; + public bool UseSsl { get; set; } = true; + public string FromAddress { get; set; } = string.Empty; + public string FromName { get; set; } = "Knowledge Agent"; + public List DefaultRecipients { get; set; } = new(); +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/OpenAIOptions.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/OpenAIOptions.cs new file mode 100644 index 0000000..4cd2ce0 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/OpenAIOptions.cs @@ -0,0 +1,13 @@ +namespace KnowledgeAgent.Infrastructure.Configuration; + +public class OpenAIOptions +{ + public const string SectionName = "OpenAI"; + + public string ApiKey { get; set; } = string.Empty; + public string Endpoint { get; set; } = string.Empty; + public string EmbeddingModel { get; set; } = "text-embedding-ada-002"; + public string CompletionModel { get; set; } = "gpt-4"; + public bool UseAzureOpenAI { get; set; } = true; + public string AzureDeploymentName { get; set; } = string.Empty; +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/SharePointOptions.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/SharePointOptions.cs new file mode 100644 index 0000000..40c89e7 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/SharePointOptions.cs @@ -0,0 +1,14 @@ +namespace KnowledgeAgent.Infrastructure.Configuration; + +public class SharePointOptions +{ + public const string SectionName = "SharePoint"; + + public string TenantId { get; set; } = string.Empty; + public string ClientId { get; set; } = string.Empty; + public string ClientSecret { get; set; } = string.Empty; + public string SiteId { get; set; } = string.Empty; + public string DriveId { get; set; } = string.Empty; + public string FolderPath { get; set; } = string.Empty; + public int SyncIntervalMinutes { get; set; } = 60; +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/VectorStoreOptions.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/VectorStoreOptions.cs new file mode 100644 index 0000000..3176ca3 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Configuration/VectorStoreOptions.cs @@ -0,0 +1,12 @@ +namespace KnowledgeAgent.Infrastructure.Configuration; + +public class VectorStoreOptions +{ + public const string SectionName = "VectorStore"; + + public string Host { get; set; } = "localhost"; + public int Port { get; set; } = 6334; + public string CollectionName { get; set; } = "knowledge_articles"; + public int VectorSize { get; set; } = 1536; + public string ApiKey { get; set; } = string.Empty; +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/DependencyInjection.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000..662e455 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/DependencyInjection.cs @@ -0,0 +1,41 @@ +using KnowledgeAgent.Core.Interfaces; +using KnowledgeAgent.Infrastructure.BackgroundJobs; +using KnowledgeAgent.Infrastructure.Configuration; +using KnowledgeAgent.Infrastructure.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace KnowledgeAgent.Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddKnowledgeAgentInfrastructure( + this IServiceCollection services, IConfiguration configuration) + { + // Register configuration options + services.Configure(configuration.GetSection(SharePointOptions.SectionName)); + services.Configure(configuration.GetSection(VectorStoreOptions.SectionName)); + services.Configure(configuration.GetSection(OpenAIOptions.SectionName)); + services.Configure(configuration.GetSection(AppInsightsOptions.SectionName)); + services.Configure(configuration.GetSection(EmailOptions.SectionName)); + + // Register services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // Register HTTP client for Application Insights API + services.AddHttpClient("AppInsights"); + + // Register background jobs + services.AddHostedService(); + services.AddHostedService(); + + return services; + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/KnowledgeAgent.Infrastructure.csproj b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/KnowledgeAgent.Infrastructure.csproj new file mode 100644 index 0000000..7cea073 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/KnowledgeAgent.Infrastructure.csproj @@ -0,0 +1,22 @@ + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/AppInsightsService.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/AppInsightsService.cs new file mode 100644 index 0000000..6e231ca --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/AppInsightsService.cs @@ -0,0 +1,137 @@ +using System.Net.Http.Json; +using System.Text.Json; +using KnowledgeAgent.Core.Interfaces; +using KnowledgeAgent.Core.Models; +using KnowledgeAgent.Infrastructure.Configuration; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace KnowledgeAgent.Infrastructure.Services; + +public class AppInsightsService : IAppInsightsService +{ + private readonly TelemetryClient _telemetryClient; + private readonly HttpClient _httpClient; + private readonly AppInsightsOptions _options; + private readonly ILogger _logger; + + public AppInsightsService( + TelemetryClient telemetryClient, + IHttpClientFactory httpClientFactory, + IOptions options, + ILogger logger) + { + _telemetryClient = telemetryClient; + _httpClient = httpClientFactory.CreateClient("AppInsights"); + _options = options.Value; + _logger = logger; + + _httpClient.BaseAddress = new Uri("https://api.applicationinsights.io/v1/"); + _httpClient.DefaultRequestHeaders.Add("x-api-key", _options.ApiKey); + } + + public async Task> GetRecentAlertsAsync( + TimeSpan lookbackPeriod, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Fetching alerts from the last {Minutes} minutes", lookbackPeriod.TotalMinutes); + + var alerts = new List(); + + try + { + var query = $"exceptions | where timestamp > ago({lookbackPeriod.TotalMinutes}m) | order by timestamp desc | take 50"; + var response = await QueryAppInsightsAsync(query, cancellationToken); + + if (response != null) + { + alerts.AddRange(ParseExceptionResults(response)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching alerts from Application Insights"); + } + + return alerts; + } + + public async Task> GetActiveExceptionsAsync( + CancellationToken cancellationToken = default) + { + var lookbackMinutes = _options.AlertLookbackMinutes; + return await GetRecentAlertsAsync(TimeSpan.FromMinutes(lookbackMinutes), cancellationToken); + } + + public void TrackEvent(string eventName, Dictionary? properties = null) + { + _telemetryClient.TrackEvent(eventName, properties); + } + + public void TrackException(Exception exception, Dictionary? properties = null) + { + var telemetry = new ExceptionTelemetry(exception); + if (properties != null) + { + foreach (var kvp in properties) + { + telemetry.Properties[kvp.Key] = kvp.Value; + } + } + _telemetryClient.TrackException(telemetry); + } + + private async Task QueryAppInsightsAsync(string query, CancellationToken cancellationToken) + { + var requestUri = $"apps/{_options.ApplicationId}/query?query={Uri.EscapeDataString(query)}"; + + var response = await _httpClient.GetAsync(requestUri, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("Application Insights query failed with status: {StatusCode}", response.StatusCode); + return null; + } + + var content = await response.Content.ReadAsStringAsync(cancellationToken); + return JsonDocument.Parse(content); + } + + private static IEnumerable ParseExceptionResults(JsonDocument response) + { + var alerts = new List(); + + if (!response.RootElement.TryGetProperty("tables", out var tables)) + return alerts; + + foreach (var table in tables.EnumerateArray()) + { + if (!table.TryGetProperty("rows", out var rows)) + continue; + + foreach (var row in rows.EnumerateArray()) + { + var alert = new AppInsightAlert + { + AlertId = Guid.NewGuid().ToString(), + AlertName = "Exception Detected", + Severity = "Error", + FiredAt = DateTime.UtcNow + }; + + if (row.GetArrayLength() > 0) + alert.ExceptionType = row[0].GetString() ?? string.Empty; + if (row.GetArrayLength() > 1) + alert.ExceptionMessage = row[1].GetString() ?? string.Empty; + if (row.GetArrayLength() > 2) + alert.StackTrace = row[2].GetString() ?? string.Empty; + + alert.Description = $"{alert.ExceptionType}: {alert.ExceptionMessage}"; + alerts.Add(alert); + } + } + + return alerts; + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/DocumentProcessor.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/DocumentProcessor.cs new file mode 100644 index 0000000..969154f --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/DocumentProcessor.cs @@ -0,0 +1,236 @@ +using System.Text; +using KnowledgeAgent.Core.Interfaces; +using KnowledgeAgent.Core.Models; +using Microsoft.Extensions.Logging; + +namespace KnowledgeAgent.Infrastructure.Services; + +public class DocumentProcessor : IDocumentProcessor +{ + private readonly ILogger _logger; + + private static readonly HashSet SupportedTextTypes = new(StringComparer.OrdinalIgnoreCase) + { + "text/plain", + "text/markdown", + "text/csv", + "text/html", + "application/json", + "application/xml" + }; + + private static readonly HashSet SupportedDocumentTypes = new(StringComparer.OrdinalIgnoreCase) + { + "application/pdf", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/msword" + }; + + public DocumentProcessor(ILogger logger) + { + _logger = logger; + } + + public async Task ExtractTextAsync( + Stream documentStream, string contentType, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Extracting text from document with content type: {ContentType}", contentType); + + if (SupportedTextTypes.Contains(contentType)) + { + return await ExtractPlainTextAsync(documentStream); + } + + if (SupportedDocumentTypes.Contains(contentType)) + { + return await ExtractFromDocumentAsync(documentStream, contentType); + } + + _logger.LogWarning("Unsupported content type: {ContentType}. Attempting plain text extraction.", contentType); + return await ExtractPlainTextAsync(documentStream); + } + + public IEnumerable ChunkDocument( + string text, Guid knowledgeArticleId, int maxChunkSize = 1000, int overlapSize = 200) + { + if (string.IsNullOrWhiteSpace(text)) + yield break; + + var sentences = SplitIntoSentences(text); + var currentChunk = new StringBuilder(); + var chunkIndex = 0; + var previousChunkEnd = string.Empty; + + foreach (var sentence in sentences) + { + if (currentChunk.Length + sentence.Length > maxChunkSize && currentChunk.Length > 0) + { + var chunkContent = currentChunk.ToString().Trim(); + yield return new DocumentChunk + { + Id = Guid.NewGuid(), + KnowledgeArticleId = knowledgeArticleId, + Content = chunkContent, + ChunkIndex = chunkIndex, + TokenCount = EstimateTokenCount(chunkContent), + Metadata = new Dictionary + { + ["chunkIndex"] = chunkIndex.ToString(), + ["articleId"] = knowledgeArticleId.ToString() + } + }; + + chunkIndex++; + previousChunkEnd = GetOverlapText(currentChunk.ToString(), overlapSize); + currentChunk.Clear(); + currentChunk.Append(previousChunkEnd); + } + + currentChunk.Append(sentence); + currentChunk.Append(' '); + } + + if (currentChunk.Length > 0) + { + var finalContent = currentChunk.ToString().Trim(); + if (!string.IsNullOrWhiteSpace(finalContent)) + { + yield return new DocumentChunk + { + Id = Guid.NewGuid(), + KnowledgeArticleId = knowledgeArticleId, + Content = finalContent, + ChunkIndex = chunkIndex, + TokenCount = EstimateTokenCount(finalContent), + Metadata = new Dictionary + { + ["chunkIndex"] = chunkIndex.ToString(), + ["articleId"] = knowledgeArticleId.ToString() + } + }; + } + } + } + + private static async Task ExtractPlainTextAsync(Stream stream) + { + using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true); + return await reader.ReadToEndAsync(); + } + + private async Task ExtractFromDocumentAsync(Stream stream, string contentType) + { + // For PDF, DOCX, XLSX, PPTX - use a simplified text extraction approach + // In production, integrate with libraries like iTextSharp, DocumentFormat.OpenXml, etc. + _logger.LogInformation("Processing document type: {ContentType}", contentType); + + return contentType switch + { + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => + await ExtractFromDocxAsync(stream), + "application/pdf" => + await ExtractFromPdfAsync(stream), + _ => await ExtractPlainTextAsync(stream) + }; + } + + private static async Task ExtractFromDocxAsync(Stream stream) + { + // Simplified DOCX extraction - reads XML content from the document + // For production, use DocumentFormat.OpenXml or similar library + using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + + try + { + using var archive = new System.IO.Compression.ZipArchive(memoryStream, System.IO.Compression.ZipArchiveMode.Read); + var documentEntry = archive.GetEntry("word/document.xml"); + if (documentEntry == null) return string.Empty; + + using var entryStream = documentEntry.Open(); + using var reader = new StreamReader(entryStream); + var xml = await reader.ReadToEndAsync(); + + // Strip XML tags to get plain text + return StripXmlTags(xml); + } + catch + { + return string.Empty; + } + } + + private static async Task ExtractFromPdfAsync(Stream stream) + { + // Simplified PDF text extraction + // For production, use a PDF library like iTextSharp or PdfPig + using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true); + var content = await reader.ReadToEndAsync(); + + // Basic PDF text extraction - extract text between stream markers + var textBuilder = new StringBuilder(); + var lines = content.Split('\n'); + foreach (var line in lines) + { + if (line.Contains("Tj") || line.Contains("TJ")) + { + var cleaned = line.Replace("Tj", "").Replace("TJ", "").Trim('(', ')', '[', ']', ' '); + if (!string.IsNullOrWhiteSpace(cleaned)) + textBuilder.AppendLine(cleaned); + } + } + + return textBuilder.Length > 0 ? textBuilder.ToString() : content; + } + + private static string StripXmlTags(string xml) + { + var result = new StringBuilder(); + var inTag = false; + + foreach (var ch in xml) + { + if (ch == '<') { inTag = true; continue; } + if (ch == '>') { inTag = false; result.Append(' '); continue; } + if (!inTag) result.Append(ch); + } + + // Clean up extra whitespace + return System.Text.RegularExpressions.Regex.Replace(result.ToString(), @"\s+", " ").Trim(); + } + + private static IEnumerable SplitIntoSentences(string text) + { + var sentenceEnders = new[] { '.', '!', '?', '\n' }; + var currentSentence = new StringBuilder(); + + foreach (var ch in text) + { + currentSentence.Append(ch); + + if (sentenceEnders.Contains(ch) && currentSentence.Length > 1) + { + yield return currentSentence.ToString().Trim(); + currentSentence.Clear(); + } + } + + if (currentSentence.Length > 0) + yield return currentSentence.ToString().Trim(); + } + + private static string GetOverlapText(string text, int overlapSize) + { + if (text.Length <= overlapSize) return text; + return text[^overlapSize..]; + } + + private static int EstimateTokenCount(string text) + { + // Rough estimation: ~4 characters per token + return text.Length / 4; + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/EmailNotificationService.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/EmailNotificationService.cs new file mode 100644 index 0000000..a1e0eef --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/EmailNotificationService.cs @@ -0,0 +1,187 @@ +using System.Net; +using System.Net.Mail; +using System.Text; +using KnowledgeAgent.Core.Interfaces; +using KnowledgeAgent.Core.Models; +using KnowledgeAgent.Infrastructure.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace KnowledgeAgent.Infrastructure.Services; + +public class EmailNotificationService : IEmailNotificationService +{ + private readonly EmailOptions _options; + private readonly ILogger _logger; + + public EmailNotificationService( + IOptions options, + ILogger logger) + { + _options = options.Value; + _logger = logger; + } + + public async Task SendAlertWithKnowledgeAsync( + AppInsightAlert alert, + IEnumerable relevantArticles, + IEnumerable recipients, + CancellationToken cancellationToken = default) + { + _logger.LogInformation("Sending alert email for alert: {AlertId}", alert.AlertId); + + var subject = $"[Knowledge Agent Alert] {alert.AlertName} - {alert.Severity}"; + var body = BuildAlertEmailBody(alert, relevantArticles); + var recipientList = recipients.Any() ? recipients.ToList() : _options.DefaultRecipients; + + await SendEmailAsync(subject, body, recipientList, cancellationToken); + } + + public async Task SendDigestEmailAsync( + IEnumerable alerts, + IEnumerable recipients, + CancellationToken cancellationToken = default) + { + var alertList = alerts.ToList(); + _logger.LogInformation("Sending digest email with {Count} alerts", alertList.Count); + + var subject = $"[Knowledge Agent] Alert Digest - {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC"; + var body = BuildDigestEmailBody(alertList); + var recipientList = recipients.Any() ? recipients.ToList() : _options.DefaultRecipients; + + await SendEmailAsync(subject, body, recipientList, cancellationToken); + } + + private async Task SendEmailAsync( + string subject, string body, List recipients, CancellationToken cancellationToken) + { + try + { + using var smtpClient = new SmtpClient(_options.SmtpHost, _options.SmtpPort) + { + Credentials = new NetworkCredential(_options.SmtpUsername, _options.SmtpPassword), + EnableSsl = _options.UseSsl + }; + + var mailMessage = new MailMessage + { + From = new MailAddress(_options.FromAddress, _options.FromName), + Subject = subject, + Body = body, + IsBodyHtml = true + }; + + foreach (var recipient in recipients) + { + mailMessage.To.Add(recipient); + } + + await smtpClient.SendMailAsync(mailMessage, cancellationToken); + _logger.LogInformation("Email sent successfully to {Count} recipients", recipients.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending email notification"); + throw; + } + } + + private static string BuildAlertEmailBody(AppInsightAlert alert, IEnumerable articles) + { + var sb = new StringBuilder(); + sb.AppendLine(""); + sb.AppendLine("
"); + + // Alert Header + sb.AppendLine($"
"); + sb.AppendLine($"

Application Insight Alert

"); + sb.AppendLine($"

Alert: {alert.AlertName}

"); + sb.AppendLine($"

Severity: {alert.Severity}

"); + sb.AppendLine($"

Time: {alert.FiredAt:yyyy-MM-dd HH:mm:ss} UTC

"); + sb.AppendLine("
"); + + // Alert Details + sb.AppendLine("
"); + sb.AppendLine("

Alert Details

"); + sb.AppendLine($"

Description: {alert.Description}

"); + if (!string.IsNullOrEmpty(alert.ExceptionType)) + sb.AppendLine($"

Exception Type: {alert.ExceptionType}

"); + if (!string.IsNullOrEmpty(alert.ExceptionMessage)) + sb.AppendLine($"

Message: {alert.ExceptionMessage}

"); + if (!string.IsNullOrEmpty(alert.AffectedResource)) + sb.AppendLine($"

Affected Resource: {alert.AffectedResource}

"); + sb.AppendLine("
"); + + // Knowledge Articles + var articleList = articles.ToList(); + if (articleList.Count > 0) + { + sb.AppendLine("
"); + sb.AppendLine("

Related Knowledge Articles

"); + sb.AppendLine("

The following knowledge articles may help resolve this issue:

"); + + foreach (var article in articleList) + { + sb.AppendLine("
"); + sb.AppendLine($"

{article.Title}

"); + sb.AppendLine($"

Relevance Score: {article.Score:P0}

"); + sb.AppendLine($"

{TruncateContent(article.Content, 300)}

"); + sb.AppendLine("
"); + } + + sb.AppendLine("
"); + } + else + { + sb.AppendLine("
"); + sb.AppendLine("

Knowledge Articles

"); + sb.AppendLine("

No directly relevant knowledge articles were found for this alert.

"); + sb.AppendLine("
"); + } + + sb.AppendLine("

This email was generated by the SharePoint Knowledge Agent. " + + "Knowledge articles are sourced from your SharePoint document library.

"); + sb.AppendLine("
"); + + return sb.ToString(); + } + + private static string BuildDigestEmailBody(List alerts) + { + var sb = new StringBuilder(); + sb.AppendLine(""); + sb.AppendLine("
"); + sb.AppendLine($"

Alert Digest - {alerts.Count} Alerts

"); + sb.AppendLine(""); + sb.AppendLine(""); + + foreach (var alert in alerts) + { + sb.AppendLine($"" + + $""); + } + + sb.AppendLine("
TimeAlertSeverityDescription
{alert.FiredAt:HH:mm:ss}{alert.AlertName}{alert.Severity}{TruncateContent(alert.Description, 100)}
"); + return sb.ToString(); + } + + private static string TruncateContent(string content, int maxLength) + { + if (string.IsNullOrEmpty(content)) return string.Empty; + return content.Length <= maxLength ? content : content[..maxLength] + "..."; + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/EmbeddingService.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/EmbeddingService.cs new file mode 100644 index 0000000..b3457d6 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/EmbeddingService.cs @@ -0,0 +1,77 @@ +using System.ClientModel; +using Azure; +using Azure.AI.OpenAI; +using KnowledgeAgent.Core.Interfaces; +using KnowledgeAgent.Infrastructure.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OpenAI.Embeddings; + +namespace KnowledgeAgent.Infrastructure.Services; + +public class EmbeddingService : IEmbeddingService +{ + private readonly EmbeddingClient _embeddingClient; + private readonly OpenAIOptions _options; + private readonly ILogger _logger; + + public EmbeddingService( + IOptions options, + ILogger logger) + { + _options = options.Value; + _logger = logger; + + if (_options.UseAzureOpenAI) + { + var azureClient = new AzureOpenAIClient( + new Uri(_options.Endpoint), + new ApiKeyCredential(_options.ApiKey)); + _embeddingClient = azureClient.GetEmbeddingClient(_options.EmbeddingModel); + } + else + { + var openAiClient = new OpenAI.OpenAIClient(new ApiKeyCredential(_options.ApiKey)); + _embeddingClient = openAiClient.GetEmbeddingClient(_options.EmbeddingModel); + } + } + + public async Task GenerateEmbeddingAsync( + string text, CancellationToken cancellationToken = default) + { + _logger.LogDebug("Generating embedding for text of length {Length}", text.Length); + + try + { + var response = await _embeddingClient.GenerateEmbeddingAsync(text); + var embedding = response.Value; + return embedding.ToFloats().ToArray(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating embedding"); + throw; + } + } + + public async Task> GenerateEmbeddingsAsync( + IEnumerable texts, CancellationToken cancellationToken = default) + { + var textList = texts.ToList(); + _logger.LogInformation("Generating embeddings for {Count} texts", textList.Count); + + try + { + var response = await _embeddingClient.GenerateEmbeddingsAsync(textList); + return response.Value + .OrderBy(e => e.Index) + .Select(e => e.ToFloats().ToArray()) + .ToList(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating batch embeddings"); + throw; + } + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/KnowledgeAgentOrchestrator.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/KnowledgeAgentOrchestrator.cs new file mode 100644 index 0000000..db3660d --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/KnowledgeAgentOrchestrator.cs @@ -0,0 +1,193 @@ +using KnowledgeAgent.Core.Interfaces; +using KnowledgeAgent.Core.Models; +using KnowledgeAgent.Infrastructure.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace KnowledgeAgent.Infrastructure.Services; + +public class KnowledgeAgentOrchestrator : IKnowledgeAgentOrchestrator +{ + private readonly ISharePointService _sharePointService; + private readonly IKnowledgeArticleService _knowledgeArticleService; + private readonly IAppInsightsService _appInsightsService; + private readonly IEmailNotificationService _emailNotificationService; + private readonly SharePointOptions _sharePointOptions; + private readonly EmailOptions _emailOptions; + private readonly ILogger _logger; + + private DateTime _lastSyncTime = DateTime.MinValue; + + public KnowledgeAgentOrchestrator( + ISharePointService sharePointService, + IKnowledgeArticleService knowledgeArticleService, + IAppInsightsService appInsightsService, + IEmailNotificationService emailNotificationService, + IOptions sharePointOptions, + IOptions emailOptions, + ILogger logger) + { + _sharePointService = sharePointService; + _knowledgeArticleService = knowledgeArticleService; + _appInsightsService = appInsightsService; + _emailNotificationService = emailNotificationService; + _sharePointOptions = sharePointOptions.Value; + _emailOptions = emailOptions.Value; + _logger = logger; + } + + public async Task SyncSharePointDocumentsAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("Starting SharePoint document sync"); + + try + { + IEnumerable documents; + + if (_lastSyncTime == DateTime.MinValue) + { + documents = await _sharePointService.GetDocumentsAsync( + _sharePointOptions.SiteId, + _sharePointOptions.DriveId, + cancellationToken); + } + else + { + documents = await _sharePointService.GetModifiedDocumentsAsync( + _sharePointOptions.SiteId, + _sharePointOptions.DriveId, + _lastSyncTime, + cancellationToken); + } + + var documentList = documents.ToList(); + _logger.LogInformation("Found {Count} documents to process", documentList.Count); + + foreach (var document in documentList) + { + try + { + await using var contentStream = await _sharePointService.DownloadDocumentAsync( + document.DriveId, document.Id, cancellationToken) + as Stream ?? Stream.Null; + + using var memoryStream = new MemoryStream(); + await contentStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + await _knowledgeArticleService.CreateFromDocumentAsync(document, memoryStream, cancellationToken); + + _logger.LogInformation("Successfully processed document: {DocumentName}", document.Name); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing document: {DocumentName}", document.Name); + } + } + + _lastSyncTime = DateTime.UtcNow; + _logger.LogInformation("SharePoint document sync completed. Processed {Count} documents", documentList.Count); + + _appInsightsService.TrackEvent("SharePointSyncCompleted", new Dictionary + { + ["DocumentCount"] = documentList.Count.ToString(), + ["SyncTime"] = _lastSyncTime.ToString("O") + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during SharePoint document sync"); + _appInsightsService.TrackException(ex, new Dictionary + { + ["Operation"] = "SharePointSync" + }); + throw; + } + } + + public async Task ProcessAlertAsync(AppInsightAlert alert, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Processing alert: {AlertId} - {AlertName}", alert.AlertId, alert.AlertName); + + try + { + // Build a search query from the alert details + var searchQuery = BuildSearchQuery(alert); + + // Search knowledge base for relevant articles + var relevantArticles = await _knowledgeArticleService.SearchKnowledgeAsync( + searchQuery, topK: 5, cancellationToken: cancellationToken); + + var articleList = relevantArticles.ToList(); + _logger.LogInformation("Found {Count} relevant knowledge articles for alert", articleList.Count); + + // Send email notification with relevant knowledge articles + await _emailNotificationService.SendAlertWithKnowledgeAsync( + alert, articleList, _emailOptions.DefaultRecipients, cancellationToken); + + _appInsightsService.TrackEvent("AlertProcessed", new Dictionary + { + ["AlertId"] = alert.AlertId, + ["RelevantArticlesCount"] = articleList.Count.ToString() + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing alert: {AlertId}", alert.AlertId); + _appInsightsService.TrackException(ex, new Dictionary + { + ["AlertId"] = alert.AlertId, + ["Operation"] = "ProcessAlert" + }); + throw; + } + } + + public async Task RunMonitoringCycleAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("Running monitoring cycle"); + + try + { + var alerts = await _appInsightsService.GetActiveExceptionsAsync(cancellationToken); + var alertList = alerts.ToList(); + + if (alertList.Count == 0) + { + _logger.LogDebug("No active alerts found in monitoring cycle"); + return; + } + + _logger.LogInformation("Found {Count} active alerts to process", alertList.Count); + + foreach (var alert in alertList) + { + await ProcessAlertAsync(alert, cancellationToken); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during monitoring cycle"); + _appInsightsService.TrackException(ex, new Dictionary + { + ["Operation"] = "MonitoringCycle" + }); + } + } + + private static string BuildSearchQuery(AppInsightAlert alert) + { + var queryParts = new List(); + + if (!string.IsNullOrEmpty(alert.ExceptionType)) + queryParts.Add(alert.ExceptionType); + if (!string.IsNullOrEmpty(alert.ExceptionMessage)) + queryParts.Add(alert.ExceptionMessage); + if (!string.IsNullOrEmpty(alert.Description)) + queryParts.Add(alert.Description); + if (!string.IsNullOrEmpty(alert.AffectedResource)) + queryParts.Add(alert.AffectedResource); + + return string.Join(" ", queryParts); + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/KnowledgeArticleService.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/KnowledgeArticleService.cs new file mode 100644 index 0000000..63be2eb --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/KnowledgeArticleService.cs @@ -0,0 +1,149 @@ +using System.Collections.Concurrent; +using KnowledgeAgent.Core.Interfaces; +using KnowledgeAgent.Core.Models; +using Microsoft.Extensions.Logging; + +namespace KnowledgeAgent.Infrastructure.Services; + +public class KnowledgeArticleService : IKnowledgeArticleService +{ + private readonly IDocumentProcessor _documentProcessor; + private readonly IEmbeddingService _embeddingService; + private readonly IVectorStoreService _vectorStoreService; + private readonly ILogger _logger; + + // In-memory store for simplicity; in production, use a database + private static readonly ConcurrentDictionary ArticleStore = new(); + + public KnowledgeArticleService( + IDocumentProcessor documentProcessor, + IEmbeddingService embeddingService, + IVectorStoreService vectorStoreService, + ILogger logger) + { + _documentProcessor = documentProcessor; + _embeddingService = embeddingService; + _vectorStoreService = vectorStoreService; + _logger = logger; + } + + public async Task CreateFromDocumentAsync( + SharePointDocument document, Stream content, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Creating knowledge article from document: {DocumentName}", document.Name); + + var article = new KnowledgeArticle + { + Title = Path.GetFileNameWithoutExtension(document.Name), + SourceDocumentId = document.Id, + SourceDocumentName = document.Name, + SharePointSiteId = document.SiteId, + SharePointDriveId = document.DriveId, + Status = ArticleStatus.Processing + }; + + try + { + var text = await _documentProcessor.ExtractTextAsync(content, document.ContentType, cancellationToken); + article.Content = text; + article.Summary = GenerateSummary(text); + + ArticleStore[article.Id] = article; + + await ProcessAndIndexDocumentAsync(document, content, cancellationToken); + + article.Status = ArticleStatus.Indexed; + article.UpdatedAt = DateTime.UtcNow; + ArticleStore[article.Id] = article; + + _logger.LogInformation("Successfully created and indexed knowledge article: {ArticleId}", article.Id); + } + catch (Exception ex) + { + article.Status = ArticleStatus.Failed; + ArticleStore[article.Id] = article; + _logger.LogError(ex, "Error creating knowledge article from document: {DocumentName}", document.Name); + throw; + } + + return article; + } + + public async Task> SearchKnowledgeAsync( + string query, int topK = 5, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Searching knowledge base for: {Query}", query); + + var queryEmbedding = await _embeddingService.GenerateEmbeddingAsync(query, cancellationToken); + var results = await _vectorStoreService.SearchAsync(queryEmbedding, topK, cancellationToken: cancellationToken); + + var enrichedResults = results.Select(r => + { + if (ArticleStore.TryGetValue(r.KnowledgeArticleId, out var article)) + { + r.Title = article.Title; + } + return r; + }); + + return enrichedResults; + } + + public Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + ArticleStore.TryGetValue(id, out var article); + return Task.FromResult(article); + } + + public Task> GetAllAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult(ArticleStore.Values.AsEnumerable()); + } + + public async Task ProcessAndIndexDocumentAsync( + SharePointDocument document, Stream content, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Processing and indexing document: {DocumentName}", document.Name); + + content.Position = 0; + var text = await _documentProcessor.ExtractTextAsync(content, document.ContentType, cancellationToken); + + var articleId = ArticleStore.Values + .FirstOrDefault(a => a.SourceDocumentId == document.Id)?.Id ?? Guid.NewGuid(); + + var chunks = _documentProcessor.ChunkDocument(text, articleId).ToList(); + + if (chunks.Count == 0) + { + _logger.LogWarning("No chunks generated from document: {DocumentName}", document.Name); + return; + } + + // Generate embeddings for all chunks + var chunkTexts = chunks.Select(c => c.Content).ToList(); + var embeddings = await _embeddingService.GenerateEmbeddingsAsync(chunkTexts, cancellationToken); + + for (int i = 0; i < chunks.Count; i++) + { + chunks[i].Embedding = embeddings[i]; + } + + // Delete existing chunks and upsert new ones + await _vectorStoreService.DeleteByArticleIdAsync(articleId, cancellationToken); + await _vectorStoreService.UpsertChunksAsync(chunks, cancellationToken); + + _logger.LogInformation("Successfully indexed {ChunkCount} chunks for document: {DocumentName}", + chunks.Count, document.Name); + } + + private static string GenerateSummary(string text, int maxLength = 500) + { + if (string.IsNullOrWhiteSpace(text)) return string.Empty; + if (text.Length <= maxLength) return text; + + var truncated = text[..maxLength]; + var lastSentenceEnd = truncated.LastIndexOfAny(new[] { '.', '!', '?' }); + + return lastSentenceEnd > 0 ? truncated[..(lastSentenceEnd + 1)] : truncated + "..."; + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/QdrantVectorStoreService.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/QdrantVectorStoreService.cs new file mode 100644 index 0000000..bdd06e8 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/QdrantVectorStoreService.cs @@ -0,0 +1,163 @@ +using KnowledgeAgent.Core.Interfaces; +using KnowledgeAgent.Core.Models; +using KnowledgeAgent.Infrastructure.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Qdrant.Client; +using Qdrant.Client.Grpc; + +namespace KnowledgeAgent.Infrastructure.Services; + +public class QdrantVectorStoreService : IVectorStoreService +{ + private readonly QdrantClient _client; + private readonly VectorStoreOptions _options; + private readonly ILogger _logger; + + public QdrantVectorStoreService( + IOptions options, + ILogger logger) + { + _options = options.Value; + _logger = logger; + + _client = new QdrantClient( + host: _options.Host, + port: _options.Port, + apiKey: string.IsNullOrEmpty(_options.ApiKey) ? null : _options.ApiKey); + } + + public async Task InitializeAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("Initializing vector store collection: {Collection}", _options.CollectionName); + + try + { + var collections = await _client.ListCollectionsAsync(cancellationToken); + var collectionExists = collections.Any(c => c == _options.CollectionName); + + if (!collectionExists) + { + await _client.CreateCollectionAsync( + _options.CollectionName, + new VectorParams + { + Size = (ulong)_options.VectorSize, + Distance = Distance.Cosine + }, + cancellationToken: cancellationToken); + + _logger.LogInformation("Created vector store collection: {Collection}", _options.CollectionName); + } + else + { + _logger.LogInformation("Vector store collection already exists: {Collection}", _options.CollectionName); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error initializing vector store"); + throw; + } + } + + public async Task UpsertChunksAsync( + IEnumerable chunks, CancellationToken cancellationToken = default) + { + var chunkList = chunks.ToList(); + _logger.LogInformation("Upserting {Count} chunks to vector store", chunkList.Count); + + try + { + var points = chunkList.Select(chunk => new PointStruct + { + Id = new PointId { Uuid = chunk.Id.ToString() }, + Vectors = chunk.Embedding, + Payload = + { + ["knowledge_article_id"] = chunk.KnowledgeArticleId.ToString(), + ["content"] = chunk.Content, + ["chunk_index"] = chunk.ChunkIndex.ToString(), + ["token_count"] = chunk.TokenCount.ToString() + } + }).ToList(); + + await _client.UpsertAsync( + _options.CollectionName, + points, + cancellationToken: cancellationToken); + + _logger.LogInformation("Successfully upserted {Count} chunks", chunkList.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error upserting chunks to vector store"); + throw; + } + } + + public async Task> SearchAsync( + float[] queryEmbedding, int topK = 5, float scoreThreshold = 0.7f, + CancellationToken cancellationToken = default) + { + _logger.LogDebug("Searching vector store with top-K={TopK}, threshold={Threshold}", topK, scoreThreshold); + + try + { + var searchResults = await _client.SearchAsync( + _options.CollectionName, + queryEmbedding, + limit: (ulong)topK, + scoreThreshold: scoreThreshold, + cancellationToken: cancellationToken); + + return searchResults.Select(r => new SearchResult + { + KnowledgeArticleId = Guid.Parse(r.Payload["knowledge_article_id"].StringValue), + Content = r.Payload["content"].StringValue, + Score = r.Score, + Metadata = new Dictionary + { + ["chunk_index"] = r.Payload["chunk_index"].StringValue, + ["token_count"] = r.Payload["token_count"].StringValue + } + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error searching vector store"); + throw; + } + } + + public async Task DeleteByArticleIdAsync(Guid knowledgeArticleId, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Deleting chunks for article {ArticleId}", knowledgeArticleId); + + try + { + await _client.DeleteAsync( + _options.CollectionName, + new Filter + { + Must = + { + new Condition + { + Field = new FieldCondition + { + Key = "knowledge_article_id", + Match = new Match { Text = knowledgeArticleId.ToString() } + } + } + } + }, + cancellationToken: cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting chunks from vector store"); + throw; + } + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/SharePointService.cs b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/SharePointService.cs new file mode 100644 index 0000000..ccef783 --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.Infrastructure/Services/SharePointService.cs @@ -0,0 +1,154 @@ +using Azure.Identity; +using KnowledgeAgent.Core.Interfaces; +using KnowledgeAgent.Core.Models; +using KnowledgeAgent.Infrastructure.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Graph; +using Microsoft.Graph.Models; + +namespace KnowledgeAgent.Infrastructure.Services; + +public class SharePointService : ISharePointService +{ + private readonly GraphServiceClient _graphClient; + private readonly SharePointOptions _options; + private readonly ILogger _logger; + + public SharePointService( + IOptions options, + ILogger logger) + { + _options = options.Value; + _logger = logger; + + var credential = new ClientSecretCredential( + _options.TenantId, + _options.ClientId, + _options.ClientSecret); + + _graphClient = new GraphServiceClient(credential, new[] { "https://graph.microsoft.com/.default" }); + } + + public async Task> GetDocumentsAsync( + string siteId, string driveId, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Fetching documents from SharePoint site {SiteId}, drive {DriveId}", siteId, driveId); + + var documents = new List(); + + try + { + var driveItems = await _graphClient.Drives[driveId].Items["root"].Children + .GetAsync(cancellationToken: cancellationToken); + + if (driveItems?.Value == null) return documents; + + foreach (var item in driveItems.Value) + { + if (item.File != null) + { + documents.Add(MapToSharePointDocument(item, siteId, driveId)); + } + else if (item.Folder != null) + { + var folderDocs = await GetFolderDocumentsRecursiveAsync(driveId, item.Id!, siteId, cancellationToken); + documents.AddRange(folderDocs); + } + } + + _logger.LogInformation("Found {Count} documents in SharePoint", documents.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching documents from SharePoint"); + throw; + } + + return documents; + } + + public async Task DownloadDocumentAsync( + string driveId, string itemId, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Downloading document {ItemId} from drive {DriveId}", itemId, driveId); + + var stream = await _graphClient.Drives[driveId].Items[itemId].Content + .GetAsync(cancellationToken: cancellationToken); + + if (stream == null) + throw new InvalidOperationException($"Failed to download document {itemId}"); + + return stream; + } + + public async Task GetDocumentMetadataAsync( + string driveId, string itemId, CancellationToken cancellationToken = default) + { + try + { + var item = await _graphClient.Drives[driveId].Items[itemId] + .GetAsync(cancellationToken: cancellationToken); + + if (item == null) return null; + + return MapToSharePointDocument(item, string.Empty, driveId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching document metadata for {ItemId}", itemId); + return null; + } + } + + public async Task> GetModifiedDocumentsAsync( + string siteId, string driveId, DateTime since, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Fetching documents modified since {Since} from SharePoint", since); + + var allDocuments = await GetDocumentsAsync(siteId, driveId, cancellationToken); + return allDocuments.Where(d => d.LastModified > since); + } + + private async Task> GetFolderDocumentsRecursiveAsync( + string driveId, string folderId, string siteId, CancellationToken cancellationToken) + { + var documents = new List(); + + var children = await _graphClient.Drives[driveId].Items[folderId].Children + .GetAsync(cancellationToken: cancellationToken); + + if (children?.Value == null) return documents; + + foreach (var item in children.Value) + { + if (item.File != null) + { + documents.Add(MapToSharePointDocument(item, siteId, driveId)); + } + else if (item.Folder != null) + { + var subDocs = await GetFolderDocumentsRecursiveAsync(driveId, item.Id!, siteId, cancellationToken); + documents.AddRange(subDocs); + } + } + + return documents; + } + + private static SharePointDocument MapToSharePointDocument(DriveItem item, string siteId, string driveId) + { + return new SharePointDocument + { + Id = item.Id ?? string.Empty, + Name = item.Name ?? string.Empty, + ContentType = item.File?.MimeType ?? string.Empty, + Size = item.Size ?? 0, + WebUrl = item.WebUrl ?? string.Empty, + DriveId = driveId, + SiteId = siteId, + LastModified = item.LastModifiedDateTime?.UtcDateTime ?? DateTime.MinValue, + LastModifiedBy = item.LastModifiedBy?.User?.DisplayName ?? string.Empty + }; + } +} diff --git a/src/Services/KnowledgeAgent/KnowledgeAgent.sln b/src/Services/KnowledgeAgent/KnowledgeAgent.sln new file mode 100644 index 0000000..97d41fa --- /dev/null +++ b/src/Services/KnowledgeAgent/KnowledgeAgent.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KnowledgeAgent.API", "KnowledgeAgent.API\KnowledgeAgent.API.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KnowledgeAgent.Core", "KnowledgeAgent.Core\KnowledgeAgent.Core.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KnowledgeAgent.Infrastructure", "KnowledgeAgent.Infrastructure\KnowledgeAgent.Infrastructure.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/Services/KnowledgeAgent/README.md b/src/Services/KnowledgeAgent/README.md new file mode 100644 index 0000000..015137c --- /dev/null +++ b/src/Services/KnowledgeAgent/README.md @@ -0,0 +1,200 @@ +# SharePoint Knowledge Agent + +An AI-powered agent built with **C# .NET 8** that reads documents from SharePoint, creates Knowledge Articles (KA), stores them in a vector database (Qdrant), and integrates with Azure Application Insights to automatically retrieve relevant knowledge when issues are detected and send email notifications. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ SharePoint Knowledge Agent │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────────┐ ┌───────────────────┐ │ +│ │ SharePoint │───▶│ Document Processor│───▶│ Embedding Service │ │ +│ │ Service │ │ (Extract & Chunk) │ │ (Azure OpenAI) │ │ +│ └──────────────┘ └──────────────────┘ └─────────┬─────────┘ │ +│ │ │ │ +│ │ (Microsoft Graph API) ▼ │ +│ │ ┌───────────────────┐ │ +│ │ │ Vector Store │ │ +│ │ │ (Qdrant) │ │ +│ │ └─────────┬─────────┘ │ +│ │ │ │ +│ ┌──────────────┐ ┌─────────▼─────────┐ │ +│ │ App Insights │───────────────────────────▶│ Knowledge Article │ │ +│ │ Monitor │ (Search for relevant │ Service │ │ +│ └──────┬───────┘ articles on alerts) └───────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────┐ │ +│ │ Email │ (Send alerts with relevant Knowledge Articles) │ +│ │ Notification │ │ +│ └──────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Features + +- **SharePoint Document Ingestion**: Connects to SharePoint via Microsoft Graph API to read documents from specified sites/drives +- **Document Processing**: Extracts text from various document formats (PDF, DOCX, TXT, HTML, CSV, etc.) and chunks them for embedding +- **Vector Storage**: Stores document embeddings in Qdrant vector database for semantic search +- **Knowledge Article Management**: Creates, indexes, and retrieves knowledge articles +- **Application Insights Integration**: Monitors for exceptions/alerts and correlates them with knowledge articles +- **Email Notifications**: Sends rich HTML emails with alert details and relevant knowledge articles +- **Background Processing**: Automated periodic syncing from SharePoint and alert monitoring +- **Webhook Support**: Accepts Application Insights alert webhooks for real-time processing + +## Prerequisites + +- .NET 8 SDK +- Qdrant vector database (included in docker-compose) +- Azure AD App Registration (for SharePoint access via Microsoft Graph) +- Azure OpenAI or OpenAI API key (for embeddings) +- Azure Application Insights instance +- SMTP server for email notifications + +## Configuration + +### Required Azure AD Permissions (Microsoft Graph) + +Register an application in Azure AD with the following permissions: +- `Sites.Read.All` - Read SharePoint sites +- `Files.Read.All` - Read files in SharePoint + +### Environment Variables + +| Variable | Description | +|----------|-------------| +| `SHAREPOINT_TENANT_ID` | Azure AD tenant ID | +| `SHAREPOINT_CLIENT_ID` | Azure AD app client ID | +| `SHAREPOINT_CLIENT_SECRET` | Azure AD app client secret | +| `SHAREPOINT_SITE_ID` | Target SharePoint site ID | +| `SHAREPOINT_DRIVE_ID` | Target SharePoint drive ID | +| `OPENAI_API_KEY` | Azure OpenAI / OpenAI API key | +| `OPENAI_ENDPOINT` | Azure OpenAI endpoint URL | +| `APPINSIGHTS_CONNECTION_STRING` | App Insights connection string | +| `APPINSIGHTS_API_KEY` | App Insights REST API key | +| `APPINSIGHTS_APPLICATION_ID` | App Insights application ID | +| `SMTP_HOST` | SMTP server host | +| `SMTP_USERNAME` | SMTP username | +| `SMTP_PASSWORD` | SMTP password | +| `EMAIL_FROM_ADDRESS` | Sender email address | + +## Getting Started + +### 1. Run with Docker Compose + +```bash +# Set environment variables in .env file +cp .env.example .env +# Edit .env with your configuration + +# Start the services +docker compose up --build +``` + +### 2. Run Locally + +```bash +# Ensure Qdrant is running +docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant:latest + +# Restore and run +cd KnowledgeAgent.API +dotnet restore +dotnet run +``` + +### 3. Access the API + +- Swagger UI: `http://localhost:5010` (Docker) or `http://localhost:5000` (local) +- Health Check: `GET /health` + +## API Endpoints + +### Knowledge Management + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/api/knowledge/search` | Semantic search across knowledge articles | +| `GET` | `/api/knowledge/articles` | List all knowledge articles | +| `GET` | `/api/knowledge/articles/{id}` | Get a specific article | + +### Agent Operations + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/api/agent/sync` | Trigger manual SharePoint sync | +| `POST` | `/api/agent/monitor` | Trigger manual monitoring cycle | +| `POST` | `/api/agent/alerts/webhook` | Receive Application Insights alert webhook | + +### Health + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/health` | Health check endpoint | +| `GET` | `/api/health` | Detailed health status | + +## How It Works + +### Document Ingestion Flow + +1. **SharePoint Sync Job** runs periodically (configurable interval) +2. Fetches new/modified documents from the configured SharePoint site +3. **Document Processor** extracts text content and splits into chunks +4. **Embedding Service** generates vector embeddings using Azure OpenAI +5. **Vector Store** (Qdrant) indexes the embeddings for semantic search +6. Knowledge Articles are created with metadata linking back to source documents + +### Alert Processing Flow + +1. **Alert Monitoring Job** polls Application Insights for new exceptions +2. Alternatively, Application Insights sends an alert via webhook +3. Agent builds a search query from the exception details +4. **Semantic Search** finds relevant Knowledge Articles from the vector store +5. **Email Notification** is sent with alert details and related knowledge articles + +## Project Structure + +``` +KnowledgeAgent/ +├── KnowledgeAgent.API/ # ASP.NET Core Web API +│ ├── Controllers/ # API endpoints +│ ├── DTOs/ # Request/Response models +│ └── Program.cs # Application entry point +├── KnowledgeAgent.Core/ # Domain layer +│ ├── Models/ # Domain entities +│ ├── Enums/ # Status enumerations +│ └── Interfaces/ # Service contracts +├── KnowledgeAgent.Infrastructure/# Implementation layer +│ ├── Services/ # Service implementations +│ ├── Configuration/ # Options/settings classes +│ └── BackgroundJobs/ # Hosted services +├── Dockerfile # Container build +├── docker-compose.yml # Multi-container setup +└── README.md # This file +``` + +## Integration with Application Insights AI Agent + +To integrate with an existing Application Insights monitoring setup: + +1. **Configure Alert Action Group**: In Azure Portal, create an Action Group that sends a webhook to `/api/agent/alerts/webhook` +2. **Set up Alert Rules**: Configure alert rules in Application Insights for exceptions, performance issues, etc. +3. **The agent automatically**: + - Receives the alert + - Searches the knowledge base for relevant documentation + - Sends an email with both the alert details and relevant knowledge articles + +## Technology Stack + +- **Runtime**: .NET 8 +- **Web Framework**: ASP.NET Core +- **SharePoint Access**: Microsoft Graph SDK v5 +- **Vector Database**: Qdrant +- **Embeddings**: Azure OpenAI (text-embedding-ada-002) +- **Monitoring**: Azure Application Insights +- **Email**: SMTP (System.Net.Mail) +- **Authentication**: Azure Identity (ClientSecretCredential) +- **Containerization**: Docker diff --git a/src/Services/KnowledgeAgent/docker-compose.yml b/src/Services/KnowledgeAgent/docker-compose.yml new file mode 100644 index 0000000..452d88f --- /dev/null +++ b/src/Services/KnowledgeAgent/docker-compose.yml @@ -0,0 +1,48 @@ +version: '3.8' + +services: + knowledge-agent-api: + build: + context: . + dockerfile: Dockerfile + ports: + - "5010:8080" + environment: + - ASPNETCORE_ENVIRONMENT=Development + - SharePoint__TenantId=${SHAREPOINT_TENANT_ID} + - SharePoint__ClientId=${SHAREPOINT_CLIENT_ID} + - SharePoint__ClientSecret=${SHAREPOINT_CLIENT_SECRET} + - SharePoint__SiteId=${SHAREPOINT_SITE_ID} + - SharePoint__DriveId=${SHAREPOINT_DRIVE_ID} + - VectorStore__Host=qdrant + - VectorStore__Port=6334 + - OpenAI__ApiKey=${OPENAI_API_KEY} + - OpenAI__Endpoint=${OPENAI_ENDPOINT} + - AppInsights__ConnectionString=${APPINSIGHTS_CONNECTION_STRING} + - AppInsights__ApiKey=${APPINSIGHTS_API_KEY} + - AppInsights__ApplicationId=${APPINSIGHTS_APPLICATION_ID} + - Email__SmtpHost=${SMTP_HOST} + - Email__SmtpUsername=${SMTP_USERNAME} + - Email__SmtpPassword=${SMTP_PASSWORD} + - Email__FromAddress=${EMAIL_FROM_ADDRESS} + depends_on: + - qdrant + networks: + - knowledge-agent-network + + qdrant: + image: qdrant/qdrant:latest + ports: + - "6333:6333" + - "6334:6334" + volumes: + - qdrant_data:/qdrant/storage + networks: + - knowledge-agent-network + +volumes: + qdrant_data: + +networks: + knowledge-agent-network: + driver: bridge diff --git a/src/Services/KnowledgeAgent/global.json b/src/Services/KnowledgeAgent/global.json new file mode 100644 index 0000000..df2d0ef --- /dev/null +++ b/src/Services/KnowledgeAgent/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.420", + "rollForward": "latestFeature" + } +}