using ManagerService.Data; using ManagerService.DTOs; using ManagerService.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using NSwag.Annotations; using Microsoft.EntityFrameworkCore; using System; using System.Linq; using System.Threading.Tasks; namespace ManagerService.Controllers { [Authorize(Policy = ManagerService.Service.Security.Policies.Viewer)] [ApiController, Route("api/[controller]")] [OpenApiTag("AI", Description = "Assistant IA")] public class AiController : ControllerBase { /// /// Plafond IA cumulé sur toute la durée de l'essai gratuit (14 jours), distinct du /// compteur mensuel : ~30 requêtes * ~10k tokens/req estimés. Empêche qu'un essai à /// cheval sur deux mois calendaires obtienne deux fois le quota mensuel du plan. /// private const long TrialAiTokensCap = 300_000; private readonly IAssistantService _assistantService; private readonly MyInfoMateDbContext _context; private readonly ILogger _logger; public AiController( IAssistantService assistantService, MyInfoMateDbContext context, ILogger logger) { _assistantService = assistantService; _context = context; _logger = logger; } private string? GetCallerInstanceId() => User.FindFirst(ManagerService.Service.Security.ClaimTypes.InstanceId)?.Value; private bool IsSuperAdmin() => User.HasClaim(ManagerService.Service.Security.ClaimTypes.Permission, ManagerService.Service.Security.Permissions.SuperAdmin); /// /// Remet le compteur mensuel à zéro si le mois a changé, puis vérifie le quota mensuel /// du plan et, pendant l'essai gratuit, le plafond cumulé de l'essai. /// Retourne null si la requête peut passer, sinon la réponse d'erreur à renvoyer. /// private IActionResult? CheckQuota(Instance instance) { var monthKey = DateTime.UtcNow.ToString("yyyy-MM"); if (instance.AiUsageMonthKey != monthKey) { instance.AiTokensThisMonth = 0; instance.AiUsageMonthKey = monthKey; _context.SaveChanges(); } var quota = instance.AiTokensPerMonth; if (quota > 0 && instance.AiTokensThisMonth >= quota) return StatusCode(429, "Quota IA mensuel dépassé"); if (instance.IsTrialActive && instance.TrialAiTokensUsed >= TrialAiTokensCap) return StatusCode(429, "Quota IA de la période d'essai dépassé"); return null; } private void RecordUsage(Instance instance, long tokensUsed) { instance.AiTokensThisMonth += tokensUsed; if (instance.IsTrialActive) instance.TrialAiTokensUsed += tokensUsed; _context.SaveChanges(); } /// /// Traduit un texte HTML vers plusieurs langues via IA /// [HttpPost("translate")] [ProducesResponseType(typeof(AiTranslateResponse), 200)] [ProducesResponseType(403)] [ProducesResponseType(typeof(string), 500)] public async Task Translate([FromBody] AiTranslateRequest request, [FromQuery] string instanceId) { try { if (!IsSuperAdmin() && instanceId != GetCallerInstanceId()) return Forbid(); var instance = _context.Instances.FirstOrDefault(i => i.Id == instanceId); if (instance == null || !instance.IsAssistant) return Forbid(); var quotaError = CheckQuota(instance); if (quotaError != null) return quotaError; var result = await _assistantService.TranslateAsync(request); RecordUsage(instance, result.TokensUsed); return Ok(result); } catch (Exception ex) { _logger.LogError(ex, "Erreur lors de la traduction IA"); return new ObjectResult("Une erreur est survenue") { StatusCode = 500 }; } } /// /// Envoie un message à l'assistant IA, scopé à l'instance et optionnellement à une configuration /// [HttpPost("chat")] [ProducesResponseType(typeof(AiChatResponse), 200)] [ProducesResponseType(403)] [ProducesResponseType(typeof(string), 500)] public async Task Chat([FromBody] AiChatRequest request) { try { if (!IsSuperAdmin() && request.InstanceId != GetCallerInstanceId()) return Forbid(); // Vérifie que l'instance a activé la fonctionnalité assistant var instance = _context.Instances .FirstOrDefault(i => i.Id == request.InstanceId); if (instance == null || !instance.IsAssistant) return Forbid(); // Vérifie que l'app concernée a activé l'assistant // Pour AppType.Voice : fallback sur Mobile si pas d'instance Voice dédiée var appInstance = _context.ApplicationInstances .FirstOrDefault(ai => ai.InstanceId == request.InstanceId && ai.AppType == request.AppType); if (appInstance == null || !appInstance.IsAssistant) return Forbid(); var quotaError = CheckQuota(instance); if (quotaError != null) return quotaError; var result = await _assistantService.ChatAsync(request); RecordUsage(instance, result.TokensUsed); return Ok(result); } catch (Exception ex) { _logger.LogError(ex, "Erreur lors de l'appel à l'assistant IA"); return new ObjectResult("Une erreur est survenue") { StatusCode = 500 }; } } } }