From 010144395c4e19e824edab0154ccf8c581ad624c Mon Sep 17 00:00:00 2001 From: Fuji Nguyen Date: Sun, 15 Mar 2026 23:14:42 -0400 Subject: [PATCH 1/2] Add Ollama AI foundation: IAiChatService, OllamaAiService, AiController with feature flag gating --- .../Interfaces/IAiChatService.cs | 7 ++++ .../ServiceRegistration.cs | 6 ++- .../Services/OllamaAiService.cs | 28 ++++++++++++++ ...ManagementAPI.Infrastructure.Shared.csproj | 1 + .../Controllers/v1/AiController.cs | 38 +++++++++++++++++++ TalentManagementAPI.WebApi/Program.cs | 5 +++ .../TalentManagementAPI.WebApi.csproj | 1 + TalentManagementAPI.WebApi/appsettings.json | 7 +++- 8 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 TalentManagementAPI.Application/Interfaces/IAiChatService.cs create mode 100644 TalentManagementAPI.Infrastructure.Shared/Services/OllamaAiService.cs create mode 100644 TalentManagementAPI.WebApi/Controllers/v1/AiController.cs diff --git a/TalentManagementAPI.Application/Interfaces/IAiChatService.cs b/TalentManagementAPI.Application/Interfaces/IAiChatService.cs new file mode 100644 index 0000000..9b3bf3b --- /dev/null +++ b/TalentManagementAPI.Application/Interfaces/IAiChatService.cs @@ -0,0 +1,7 @@ +namespace TalentManagementAPI.Application.Interfaces +{ + public interface IAiChatService + { + Task ChatAsync(string message, string? systemPrompt = null, CancellationToken cancellationToken = default); + } +} diff --git a/TalentManagementAPI.Infrastructure.Shared/ServiceRegistration.cs b/TalentManagementAPI.Infrastructure.Shared/ServiceRegistration.cs index 0bbaf81..bffa6c8 100644 --- a/TalentManagementAPI.Infrastructure.Shared/ServiceRegistration.cs +++ b/TalentManagementAPI.Infrastructure.Shared/ServiceRegistration.cs @@ -1,4 +1,7 @@ -namespace TalentManagementAPI.Infrastructure.Shared +using TalentManagementAPI.Application.Interfaces; +using TalentManagementAPI.Infrastructure.Shared.Services; + +namespace TalentManagementAPI.Infrastructure.Shared { public static class ServiceRegistration { @@ -8,6 +11,7 @@ public static void AddSharedInfrastructure(this IServiceCollection services, ICo services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } } } diff --git a/TalentManagementAPI.Infrastructure.Shared/Services/OllamaAiService.cs b/TalentManagementAPI.Infrastructure.Shared/Services/OllamaAiService.cs new file mode 100644 index 0000000..e527908 --- /dev/null +++ b/TalentManagementAPI.Infrastructure.Shared/Services/OllamaAiService.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.AI; +using TalentManagementAPI.Application.Interfaces; + +namespace TalentManagementAPI.Infrastructure.Shared.Services +{ + public class OllamaAiService : IAiChatService + { + private readonly IChatClient _chatClient; + + public OllamaAiService(IChatClient chatClient) + { + _chatClient = chatClient; + } + + public async Task ChatAsync(string message, string? systemPrompt = null, CancellationToken cancellationToken = default) + { + var messages = new List(); + + if (!string.IsNullOrWhiteSpace(systemPrompt)) + messages.Add(new ChatMessage(ChatRole.System, systemPrompt)); + + messages.Add(new ChatMessage(ChatRole.User, message)); + + var response = await _chatClient.CompleteAsync(messages, cancellationToken: cancellationToken); + return response.Message.Text ?? string.Empty; + } + } +} diff --git a/TalentManagementAPI.Infrastructure.Shared/TalentManagementAPI.Infrastructure.Shared.csproj b/TalentManagementAPI.Infrastructure.Shared/TalentManagementAPI.Infrastructure.Shared.csproj index ae9de90..184cace 100644 --- a/TalentManagementAPI.Infrastructure.Shared/TalentManagementAPI.Infrastructure.Shared.csproj +++ b/TalentManagementAPI.Infrastructure.Shared/TalentManagementAPI.Infrastructure.Shared.csproj @@ -8,6 +8,7 @@ + diff --git a/TalentManagementAPI.WebApi/Controllers/v1/AiController.cs b/TalentManagementAPI.WebApi/Controllers/v1/AiController.cs new file mode 100644 index 0000000..4dd85bc --- /dev/null +++ b/TalentManagementAPI.WebApi/Controllers/v1/AiController.cs @@ -0,0 +1,38 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.FeatureManagement.Mvc; +using TalentManagementAPI.Application.Interfaces; + +namespace TalentManagementAPI.WebApi.Controllers.v1 +{ + [FeatureGate("AiEnabled")] + [ApiVersion("1.0")] + [AllowAnonymous] + [Route("api/v{version:apiVersion}/ai")] + public sealed class AiController : BaseApiController + { + private readonly IAiChatService _aiChatService; + + public AiController(IAiChatService aiChatService) + { + _aiChatService = aiChatService; + } + + /// + /// Send a message to the AI assistant and receive a reply. + /// + /// The chat message and optional system prompt. + /// Cancellation token. + /// The AI-generated reply. + [HttpPost("chat")] + public async Task Chat([FromBody] AiChatRequest request, CancellationToken cancellationToken) + { + var reply = await _aiChatService.ChatAsync(request.Message, request.SystemPrompt, cancellationToken); + return Ok(new AiChatResponse(reply)); + } + } + + public record AiChatRequest(string Message, string? SystemPrompt = null); + public record AiChatResponse(string Reply); +} diff --git a/TalentManagementAPI.WebApi/Program.cs b/TalentManagementAPI.WebApi/Program.cs index c20b60c..e5a11f7 100644 --- a/TalentManagementAPI.WebApi/Program.cs +++ b/TalentManagementAPI.WebApi/Program.cs @@ -19,6 +19,11 @@ builder.Services.AddApplicationLayer(); builder.Services.AddPersistenceInfrastructure(builder.Configuration); builder.Services.AddSharedInfrastructure(builder.Configuration); + // Register Ollama chat client (IChatClient) — used by OllamaAiService + // AiController is gated by [FeatureGate("AiEnabled")], so no calls are made when AI is disabled + var ollamaBaseUrl = builder.Configuration["Ollama:BaseUrl"] ?? "http://localhost:11434"; + var ollamaModel = builder.Configuration["Ollama:Model"] ?? "llama3.2"; + builder.Services.AddOllamaChatClient(ollamaModel, new Uri(ollamaBaseUrl)); builder.Services.AddEasyCachingInfrastructure(builder.Configuration); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); diff --git a/TalentManagementAPI.WebApi/TalentManagementAPI.WebApi.csproj b/TalentManagementAPI.WebApi/TalentManagementAPI.WebApi.csproj index 6961435..ee84bb5 100644 --- a/TalentManagementAPI.WebApi/TalentManagementAPI.WebApi.csproj +++ b/TalentManagementAPI.WebApi/TalentManagementAPI.WebApi.csproj @@ -41,6 +41,7 @@ all + diff --git a/TalentManagementAPI.WebApi/appsettings.json b/TalentManagementAPI.WebApi/appsettings.json index b27ecb2..f865f29 100644 --- a/TalentManagementAPI.WebApi/appsettings.json +++ b/TalentManagementAPI.WebApi/appsettings.json @@ -106,7 +106,12 @@ "ExecutionTimingIncludeHeader": true, "ExecutionTimingIncludePayload": true, "ExecutionTimingLogTimings": false, - "UseInMemoryDatabase": false + "UseInMemoryDatabase": false, + "AiEnabled": false + }, + "Ollama": { + "BaseUrl": "http://localhost:11434", + "Model": "llama3.2" }, "ApiRoles": { "EmployeeRole": "Employee", From 7f302767b398a918addff6b79e93b30297d9dd3f Mon Sep 17 00:00:00 2001 From: Fuji Nguyen Date: Tue, 17 Mar 2026 22:25:54 -0400 Subject: [PATCH 2/2] Refactor AI service to use OllamaSharp instead of Microsoft.Extensions.AI --- .../GlobalUsings.cs | 3 ++ .../ServiceRegistration.cs | 10 ++++-- .../Services/OllamaAiService.cs | 35 ++++++++++++++----- ...ManagementAPI.Infrastructure.Shared.csproj | 10 +++--- TalentManagementAPI.WebApi/Program.cs | 3 -- .../TalentManagementAPI.WebApi.csproj | 1 - 6 files changed, 42 insertions(+), 20 deletions(-) diff --git a/TalentManagementAPI.Infrastructure.Shared/GlobalUsings.cs b/TalentManagementAPI.Infrastructure.Shared/GlobalUsings.cs index cb46649..b231087 100644 --- a/TalentManagementAPI.Infrastructure.Shared/GlobalUsings.cs +++ b/TalentManagementAPI.Infrastructure.Shared/GlobalUsings.cs @@ -1,6 +1,7 @@ global using System; global using System.Collections.Generic; global using System.Linq; +global using System.Threading; global using System.Threading.Tasks; global using AutoBogus; global using Bogus; @@ -12,6 +13,8 @@ global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Options; global using MimeKit; +global using OllamaSharp; +global using OllamaSharp.Models.Chat; global using TalentManagementAPI.Application.DTOs.Email; global using TalentManagementAPI.Application.Exceptions; global using TalentManagementAPI.Application.Interfaces; diff --git a/TalentManagementAPI.Infrastructure.Shared/ServiceRegistration.cs b/TalentManagementAPI.Infrastructure.Shared/ServiceRegistration.cs index bffa6c8..f782050 100644 --- a/TalentManagementAPI.Infrastructure.Shared/ServiceRegistration.cs +++ b/TalentManagementAPI.Infrastructure.Shared/ServiceRegistration.cs @@ -5,12 +5,18 @@ namespace TalentManagementAPI.Infrastructure.Shared { public static class ServiceRegistration { - public static void AddSharedInfrastructure(this IServiceCollection services, IConfiguration _config) + public static void AddSharedInfrastructure(this IServiceCollection services, IConfiguration config) { - services.Configure(_config.GetSection("MailSettings")); + services.Configure(config.GetSection("MailSettings")); services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddSingleton(_ => + { + var baseUrl = config["Ollama:BaseUrl"] ?? "http://localhost:11434"; + var model = config["Ollama:Model"] ?? "llama3.2"; + return new OllamaApiClient(new Uri(baseUrl), model); + }); services.AddTransient(); } } diff --git a/TalentManagementAPI.Infrastructure.Shared/Services/OllamaAiService.cs b/TalentManagementAPI.Infrastructure.Shared/Services/OllamaAiService.cs index e527908..5f99c71 100644 --- a/TalentManagementAPI.Infrastructure.Shared/Services/OllamaAiService.cs +++ b/TalentManagementAPI.Infrastructure.Shared/Services/OllamaAiService.cs @@ -1,28 +1,45 @@ -using Microsoft.Extensions.AI; using TalentManagementAPI.Application.Interfaces; namespace TalentManagementAPI.Infrastructure.Shared.Services { public class OllamaAiService : IAiChatService { - private readonly IChatClient _chatClient; + private readonly IOllamaApiClient _ollamaApiClient; - public OllamaAiService(IChatClient chatClient) + public OllamaAiService(IOllamaApiClient ollamaApiClient) { - _chatClient = chatClient; + _ollamaApiClient = ollamaApiClient; } public async Task ChatAsync(string message, string? systemPrompt = null, CancellationToken cancellationToken = default) { - var messages = new List(); + var messages = new List(); if (!string.IsNullOrWhiteSpace(systemPrompt)) - messages.Add(new ChatMessage(ChatRole.System, systemPrompt)); + { + messages.Add(new Message(new ChatRole("system"), systemPrompt)); + } - messages.Add(new ChatMessage(ChatRole.User, message)); + messages.Add(new Message(new ChatRole("user"), message)); - var response = await _chatClient.CompleteAsync(messages, cancellationToken: cancellationToken); - return response.Message.Text ?? string.Empty; + var request = new ChatRequest + { + Model = _ollamaApiClient.SelectedModel, + Messages = messages, + Stream = true + }; + + var responseBuilder = new MessageBuilder(); + + await foreach (var response in _ollamaApiClient.ChatAsync(request, cancellationToken).WithCancellation(cancellationToken)) + { + if (response?.Message is not null) + { + responseBuilder.Append(response); + } + } + + return responseBuilder.HasValue ? responseBuilder.ToMessage().Content ?? string.Empty : string.Empty; } } } diff --git a/TalentManagementAPI.Infrastructure.Shared/TalentManagementAPI.Infrastructure.Shared.csproj b/TalentManagementAPI.Infrastructure.Shared/TalentManagementAPI.Infrastructure.Shared.csproj index 184cace..c68d0b9 100644 --- a/TalentManagementAPI.Infrastructure.Shared/TalentManagementAPI.Infrastructure.Shared.csproj +++ b/TalentManagementAPI.Infrastructure.Shared/TalentManagementAPI.Infrastructure.Shared.csproj @@ -8,12 +8,12 @@ - - - - - + + + + + diff --git a/TalentManagementAPI.WebApi/Program.cs b/TalentManagementAPI.WebApi/Program.cs index e5a11f7..021e473 100644 --- a/TalentManagementAPI.WebApi/Program.cs +++ b/TalentManagementAPI.WebApi/Program.cs @@ -21,9 +21,6 @@ builder.Services.AddSharedInfrastructure(builder.Configuration); // Register Ollama chat client (IChatClient) — used by OllamaAiService // AiController is gated by [FeatureGate("AiEnabled")], so no calls are made when AI is disabled - var ollamaBaseUrl = builder.Configuration["Ollama:BaseUrl"] ?? "http://localhost:11434"; - var ollamaModel = builder.Configuration["Ollama:Model"] ?? "llama3.2"; - builder.Services.AddOllamaChatClient(ollamaModel, new Uri(ollamaBaseUrl)); builder.Services.AddEasyCachingInfrastructure(builder.Configuration); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); diff --git a/TalentManagementAPI.WebApi/TalentManagementAPI.WebApi.csproj b/TalentManagementAPI.WebApi/TalentManagementAPI.WebApi.csproj index ee84bb5..6961435 100644 --- a/TalentManagementAPI.WebApi/TalentManagementAPI.WebApi.csproj +++ b/TalentManagementAPI.WebApi/TalentManagementAPI.WebApi.csproj @@ -41,7 +41,6 @@ all -