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
68 changes: 34 additions & 34 deletions blotztask-api/BlotzTask.csproj
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" Version="2.2.0-beta.4" />
<PackageReference Include="Azure.Identity" Version="1.13.2" />
<PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.2.0" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.7.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.19" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0-preview.1.25080.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.AzureKeyVault" Version="3.1.24" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0-preview.1.25080.5" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.48.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.ApplicationInsights" Version="4.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" Version="2.2.0-beta.4"/>
<PackageReference Include="Azure.Identity" Version="1.13.2"/>
<PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.2.0"/>
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.7.0"/>
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.19"/>
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0"/>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="8.0.7"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0-preview.1.25080.5"/>
<PackageReference Include="Microsoft.Extensions.Configuration.AzureKeyVault" Version="3.1.24"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0-preview.1.25080.5"/>
<PackageReference Include="Microsoft.SemanticKernel" Version="1.48.0"/>
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0"/>
<PackageReference Include="Serilog.Sinks.ApplicationInsights" Version="4.0.0"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0"/>
</ItemGroup>

<ItemGroup>
<Folder Include="Modules\ChatTaskGenerator\Plugins\" />
</ItemGroup>
<ItemGroup>
<Folder Include="Modules\ChatTaskGenerator\Plugins\"/>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,7 @@ CancellationToken ct
extractTasksFunction.Name
);

// Log additional debug information
_logger.LogDebug(
"Function details - Plugin: {PluginName}, Function: {FunctionName}, " +
"Execution Settings: {ExecutionSettings}",
extractTasksFunction.PluginName,
extractTasksFunction.Name,
JsonSerializer.Serialize(executionSettings)
);


// Log the last user message for context
var lastUserMessage = tempHistory.LastOrDefault(m => m.Role == AuthorRole.User);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using BlotzTask.Modules.DailyReminderGenerator.Dtos;
using BlotzTask.Modules.DailyReminderGenerator.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace BlotzTask.Modules.DailyReminderGenerator.Controllers;

[ApiController]
[Route("/api/[controller]")]
[Authorize]
public class AiReminderController : ControllerBase
{
private readonly AiReminderService _aiReminderService;

public AiReminderController(AiReminderService aiReminderService)
{
_aiReminderService = aiReminderService;
}

[HttpGet("today")]
[ProducesResponseType(typeof(ReminderResult), 200)]
[ProducesResponseType(204)]
[ProducesResponseType(401)]
public async Task<IActionResult> GetTodayReminder(CancellationToken ct)
{
if (!HttpContext.Items.TryGetValue("UserId", out var userIdObj) || userIdObj is not Guid userId)
return Unauthorized();

var reminder = await _aiReminderService.GenerateReminderForTodayAsync(userId, ct);

if (reminder is null) return NoContent();

return Ok(reminder);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace BlotzTask.Modules.DailyReminderGenerator.Dtos;

public class ReminderResult
{
public int? TaskId { get; set; }
public string? ReminderText { get; set; }
public double? ConfidenceScore { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using BlotzTask.Modules.Labels.DTOs;
using BlotzTask.Modules.Tasks.Enums;

namespace BlotzTask.Modules.DailyReminderGenerator.Dtos;

public class TodayTaskItemDto
{
public int Id { get; set; }
public required string Title { get; set; }
public string? Description { get; set; }
public DateTimeOffset? StartTime { get; set; }
public DateTimeOffset? EndTime { get; set; }
public bool IsDone { get; set; }
public LabelDto? Label { get; set; }
public TaskTimeType? TimeType { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.ComponentModel;
using BlotzTask.Modules.DailyReminderGenerator.Dtos;
using Microsoft.SemanticKernel;

namespace BlotzTask.Modules.DailyReminderGenerator.Plugins;

public class ReminderGenerationPlugin
{
[KernelFunction]
[Description(
"Selects the single most important task from a list of today's tasks and generates a short, friendly reminding message."
)]
public ReminderResult GenerateReminder(
[Description("The JSON array of today's tasks with Id, Title, Description, StartTime, EndTime, TimeType")]
string tasksJson
)
{
return new ReminderResult();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace BlotzTask.Modules.DailyReminderGenerator.Prompt;

public class AiReminderPrompts
{
public static string ReminderGenerationSystemMessage(DateTime currentTime)
{
return $$$"""
You are a helpful assistant that picks exactly ONE most important task for {{{currentTime:yyyy-MM-dd}}}
from the user's task list and writes a short, friendly tip.

Rules:
- Consider deadlines (EndTime, StartTime).
- Prefer tasks due today or blocking others. Ignore completed tasks.
- If tasks are vague/unrecognizable, return taskId = null.
- Output STRICT JSON only:
{
"taskId": "<int or null>",
"reminderText": "<<=140 chars>",
"confidence": 0..1
}
- Tip tone: warm, concise, motivating.
Example: "Heads up! It’s rent day today—time to keep the landlord happy."
""";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System.Text.Json;
using BlotzTask.Modules.DailyReminderGenerator.Dtos;
using BlotzTask.Modules.DailyReminderGenerator.Prompt;
using BlotzTask.Modules.Tasks.Queries.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

namespace BlotzTask.Modules.DailyReminderGenerator.Services;

public class AiReminderService
{
private readonly IChatCompletionService _chatCompletionService;
private readonly GetTasksByDateQueryHandler _getTasksByDate;
private readonly Kernel _kernel;
private readonly ILogger<AiReminderService> _logger;

public AiReminderService(
Kernel kernel,
IChatCompletionService chatCompletionService,
ILogger<AiReminderService> logger,
GetTasksByDateQueryHandler getTasksByDateQueryHandler)
{
_kernel = kernel;
_chatCompletionService = chatCompletionService;
_logger = logger;
_getTasksByDate = getTasksByDateQueryHandler;
}

public async Task<ReminderResult?> GenerateReminderForTodayAsync(Guid userId, CancellationToken ct = default)
{
var query = new GetTasksByDateQuery
{
UserId = userId,
StartDateUtc = DateTimeOffset.UtcNow.Date,
IncludeFloatingForToday = true
};
var todayTasks = (await _getTasksByDate.Handle(query, ct)).ToList();
_logger.LogInformation("Fetched {TotalTasks} tasks for today (including floating).", todayTasks.Count);

var todoTasks = todayTasks
.Where(t => !t.IsDone)
.ToList();

_logger.LogInformation("Fetched {TotalTasks} todo tasks for today (including floating).", todoTasks.Count);

if (todoTasks.Count == 0) return null;

var tasksJson = JsonSerializer.Serialize(new
{
todayUtc = DateTimeOffset.UtcNow,
tasks = todoTasks
});
_logger.LogInformation(
"Prepared tasks JSON for model. JsonLength={JsonLength}",
tasksJson.Length);


var history = new ChatHistory();
history.AddSystemMessage(AiReminderPrompts.ReminderGenerationSystemMessage(DateTimeOffset.UtcNow.UtcDateTime));

history.AddUserMessage(tasksJson);

var generateReminderFunction = _kernel.Plugins["ReminderGenerationPlugin"]["GenerateReminder"];
var reminderSettings = new PromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Required(
new[] { generateReminderFunction }
)
};

_logger.LogDebug(
"Invoking SK function. Plugin={Plugin}, Function={Function}, Settings={Settings}",
generateReminderFunction.PluginName,
generateReminderFunction.Name,
JsonSerializer.Serialize(reminderSettings));

try
{
var reminderResult = await _chatCompletionService.GetChatMessageContentsAsync(
history,
reminderSettings,
_kernel,
ct
);

var content = reminderResult.LastOrDefault()?.Content;
if (string.IsNullOrWhiteSpace(content))
{
_logger.LogWarning("Model returned empty content. Returning null (HTTP 204).");
return null;
}

var reminder = JsonSerializer.Deserialize<ReminderResult>(content!,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (reminder?.TaskId is null || string.IsNullOrWhiteSpace(reminder.ReminderText))
{
_logger.LogWarning("Reminder missing required fields. TaskId={TaskId}, HasText={HasText}",
reminder?.TaskId, !string.IsNullOrWhiteSpace(reminder?.ReminderText));
return null;
}

if (!todoTasks.Any(t => t.Id == reminder.TaskId.Value)) return null;

return reminder;
}
catch (Exception ex)
{
_logger.LogError(ex, "GenerateReminderForTodayAsync failed.");
return null;
}
}
}
Loading
Loading