Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/Services/KnowledgeAgent/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
Original file line number Diff line number Diff line change
@@ -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<AgentController> _logger;

public AgentController(
IKnowledgeAgentOrchestrator orchestrator,
ILogger<AgentController> logger)
{
_orchestrator = orchestrator;
_logger = logger;
}

[HttpPost("sync")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
public Task<IActionResult> 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);
Comment on lines +30 to +40
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Request-scoped CancellationToken passed to fire-and-forget Task.Run cancels background work

In TriggerSync and TriggerMonitoring, the cancellationToken parameter is bound to the HTTP request's HttpContext.RequestAborted. Since the controller returns Accepted() immediately, the HTTP connection closes shortly after, and ASP.NET Core signals the cancellation token. The background Task.Run receives this token both as its scheduler token (line 40/61) and passes it into the orchestrator methods (line 34/55), causing the sync/monitoring work to be cancelled almost immediately via OperationCanceledException.

Prompt for agents
In AgentController.TriggerSync (and similarly TriggerMonitoring), the CancellationToken from the HTTP request is passed into Task.Run and to the orchestrator methods. This token gets cancelled when the HTTP response is sent and the connection closes, aborting the background work.

The fix should use CancellationToken.None (or an application-lifetime token) for the background work instead of the request-scoped cancellationToken. You could inject IHostApplicationLifetime and use its ApplicationStopping token, or simply use CancellationToken.None.

Alternatively, this fire-and-forget pattern should be replaced with a proper background task queue (e.g., IBackgroundTaskQueue or Channel<T>) to avoid additional issues with using scoped services after the request scope is disposed.

Affected methods: TriggerSync (lines 25-43), TriggerMonitoring (lines 47-64) in AgentController.cs.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


return Task.FromResult<IActionResult>(Accepted(new { message = "SharePoint document sync initiated" }));
}

[HttpPost("monitor")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
public Task<IActionResult> 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<IActionResult>(Accepted(new { message = "Monitoring cycle initiated" }));
}

[HttpPost("alerts/webhook")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> 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<string, string>()
};

await _orchestrator.ProcessAlertAsync(alert, cancellationToken);

return Ok(new { message = "Alert processed successfully", alertId = alert.AlertId });
}
}
Original file line number Diff line number Diff line change
@@ -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
});
}
}
Original file line number Diff line number Diff line change
@@ -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<KnowledgeController> _logger;

public KnowledgeController(
IKnowledgeArticleService knowledgeArticleService,
ILogger<KnowledgeController> logger)
{
_knowledgeArticleService = knowledgeArticleService;
_logger = logger;
}

[HttpPost("search")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> GetArticle(Guid id, CancellationToken cancellationToken)
{
var article = await _knowledgeArticleService.GetByIdAsync(id, cancellationToken);

if (article == null)
return NotFound();

return Ok(article);
}
}
Original file line number Diff line number Diff line change
@@ -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<string, string>? CustomProperties { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace KnowledgeAgent.API.DTOs;

public class SearchRequest
{
public string Query { get; set; } = string.Empty;
public int TopK { get; set; } = 5;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace KnowledgeAgent.API.DTOs;

public class SyncRequest
{
public string? SiteId { get; set; }
public string? DriveId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\KnowledgeAgent.Core\KnowledgeAgent.Core.csproj" />
<ProjectReference Include="..\KnowledgeAgent.Infrastructure\KnowledgeAgent.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
</ItemGroup>
</Project>
58 changes: 58 additions & 0 deletions src/Services/KnowledgeAgent/KnowledgeAgent.API/Program.cs
Original file line number Diff line number Diff line change
@@ -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<IVectorStoreService>();
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();
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Information",
"KnowledgeAgent": "Trace"
}
},
"SharePoint": {
"SyncIntervalMinutes": 5
},
"AppInsights": {
"MonitoringIntervalMinutes": 2,
"AlertLookbackMinutes": 30
}
}
54 changes: 54 additions & 0 deletions src/Services/KnowledgeAgent/KnowledgeAgent.API/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"KnowledgeAgent": "Debug"
}
},
"AllowedHosts": "*",
"SharePoint": {
"TenantId": "<your-tenant-id>",
"ClientId": "<your-client-id>",
"ClientSecret": "<your-client-secret>",
"SiteId": "<your-sharepoint-site-id>",
"DriveId": "<your-sharepoint-drive-id>",
"FolderPath": "/Documents/KnowledgeBase",
"SyncIntervalMinutes": 60
},
"VectorStore": {
"Host": "localhost",
"Port": 6334,
"CollectionName": "knowledge_articles",
"VectorSize": 1536,
"ApiKey": ""
},
"OpenAI": {
"ApiKey": "<your-openai-api-key>",
"Endpoint": "https://<your-resource>.openai.azure.com/",
"EmbeddingModel": "text-embedding-ada-002",
"CompletionModel": "gpt-4",
"UseAzureOpenAI": true,
"AzureDeploymentName": "<your-deployment-name>"
},
"AppInsights": {
"ConnectionString": "<your-app-insights-connection-string>",
"InstrumentationKey": "<your-instrumentation-key>",
"ApplicationId": "<your-application-id>",
"ApiKey": "<your-api-key>",
"MonitoringIntervalMinutes": 5,
"AlertLookbackMinutes": 15
},
"Email": {
"SmtpHost": "smtp.office365.com",
"SmtpPort": 587,
"SmtpUsername": "<your-smtp-username>",
"SmtpPassword": "<your-smtp-password>",
"UseSsl": true,
"FromAddress": "knowledge-agent@yourdomain.com",
"FromName": "Knowledge Agent",
"DefaultRecipients": [
"team@yourdomain.com"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace KnowledgeAgent.Core.Models;

public enum ArticleStatus
{
Draft,
Processing,
Indexed,
Failed,
Archived
}
Loading