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/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 0bbaf81..f782050 100644 --- a/TalentManagementAPI.Infrastructure.Shared/ServiceRegistration.cs +++ b/TalentManagementAPI.Infrastructure.Shared/ServiceRegistration.cs @@ -1,13 +1,23 @@ -namespace TalentManagementAPI.Infrastructure.Shared +using TalentManagementAPI.Application.Interfaces; +using TalentManagementAPI.Infrastructure.Shared.Services; + +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 new file mode 100644 index 0000000..5f99c71 --- /dev/null +++ b/TalentManagementAPI.Infrastructure.Shared/Services/OllamaAiService.cs @@ -0,0 +1,45 @@ +using TalentManagementAPI.Application.Interfaces; + +namespace TalentManagementAPI.Infrastructure.Shared.Services +{ + public class OllamaAiService : IAiChatService + { + private readonly IOllamaApiClient _ollamaApiClient; + + public OllamaAiService(IOllamaApiClient ollamaApiClient) + { + _ollamaApiClient = ollamaApiClient; + } + + public async Task ChatAsync(string message, string? systemPrompt = null, CancellationToken cancellationToken = default) + { + var messages = new List(); + + if (!string.IsNullOrWhiteSpace(systemPrompt)) + { + messages.Add(new Message(new ChatRole("system"), systemPrompt)); + } + + messages.Add(new Message(new ChatRole("user"), message)); + + 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 ae9de90..c68d0b9 100644 --- a/TalentManagementAPI.Infrastructure.Shared/TalentManagementAPI.Infrastructure.Shared.csproj +++ b/TalentManagementAPI.Infrastructure.Shared/TalentManagementAPI.Infrastructure.Shared.csproj @@ -9,10 +9,11 @@ - - - - + + + + + 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..021e473 100644 --- a/TalentManagementAPI.WebApi/Program.cs +++ b/TalentManagementAPI.WebApi/Program.cs @@ -19,6 +19,8 @@ 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 builder.Services.AddEasyCachingInfrastructure(builder.Configuration); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); 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",