Add instance index + date update audit + ai quota to token

This commit is contained in:
Thomas Fransolet 2026-07-17 15:21:51 +02:00
parent 19a0157b19
commit f2228617ca
33 changed files with 5666 additions and 87 deletions

View File

@ -16,7 +16,7 @@ namespace ManagerService.Tests.Controllers
{
public class AiControllerTests
{
private static readonly AiChatResponse FakeResponse = new AiChatResponse { Reply = "OK" };
private static readonly AiChatResponse FakeResponse = new AiChatResponse { Reply = "OK", TokensUsed = 42 };
private AiController BuildController(MyInfoMateDbContext db, Mock<IAssistantService>? mockService = null)
{
@ -101,13 +101,13 @@ namespace ManagerService.Tests.Controllers
// ── QUOTA COUNTER ────────────────────────────────────────────────────
[Fact]
public async Task Chat_FirstRequestOfMonth_ResetsCounterAndSets1()
public async Task Chat_FirstRequestOfMonth_ResetsCounterAndAddsTokensUsed()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow,
AiRequestsThisMonth = 99, AiUsageMonthKey = "2020-01"
AiTokensThisMonth = 99, AiUsageMonthKey = "2020-01"
});
db.ApplicationInstances.Add(new ApplicationInstance
{
@ -119,19 +119,19 @@ namespace ManagerService.Tests.Controllers
await BuildController(db).Chat(MakeRequest("i1"));
var inst = db.Instances.First();
Assert.Equal(1, inst.AiRequestsThisMonth);
Assert.Equal(42, inst.AiTokensThisMonth);
Assert.Equal(DateTime.UtcNow.ToString("yyyy-MM"), inst.AiUsageMonthKey);
}
[Fact]
public async Task Chat_SameMonth_IncrementsCounter()
public async Task Chat_SameMonth_AddsTokensUsedToCounter()
{
using var db = DbContextFactory.Create();
var monthKey = DateTime.UtcNow.ToString("yyyy-MM");
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow,
AiRequestsThisMonth = 3, AiUsageMonthKey = monthKey
AiTokensThisMonth = 3, AiUsageMonthKey = monthKey
});
db.ApplicationInstances.Add(new ApplicationInstance
{
@ -142,7 +142,34 @@ namespace ManagerService.Tests.Controllers
await BuildController(db).Chat(MakeRequest("i1"));
Assert.Equal(4, db.Instances.First().AiRequestsThisMonth);
Assert.Equal(45, db.Instances.First().AiTokensThisMonth);
}
[Fact]
public async Task Chat_QuotaAlreadyReached_ReturnsTooManyRequestsWithoutCallingAssistantService()
{
using var db = DbContextFactory.Create();
var monthKey = DateTime.UtcNow.ToString("yyyy-MM");
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow,
AiTokensThisMonth = 100, AiTokensPerMonth = 100, AiUsageMonthKey = monthKey
});
db.ApplicationInstances.Add(new ApplicationInstance
{
Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true,
Languages = new List<string>()
});
db.SaveChanges();
var mockService = new Mock<IAssistantService>();
mockService.Setup(s => s.ChatAsync(It.IsAny<AiChatRequest>())).ReturnsAsync(FakeResponse);
var result = await BuildController(db, mockService).Chat(MakeRequest("i1"));
var status = Assert.IsType<ObjectResult>(result);
Assert.Equal(429, status.StatusCode);
mockService.Verify(s => s.ChatAsync(It.IsAny<AiChatRequest>()), Times.Never);
}
// ── NOMINAL ──────────────────────────────────────────────────────────

View File

@ -130,7 +130,7 @@ namespace ManagerService.Tests.Controllers
var ok = Assert.IsType<OkObjectResult>(result);
var dto = Assert.IsType<InstanceQuotaDTO>(ok.Value);
Assert.Equal(0, dto.storageQuotaBytes);
Assert.Equal(0, dto.aiRequestsPerMonth);
Assert.Equal(0, dto.aiTokensPerMonth);
}
[Fact]
@ -141,7 +141,7 @@ namespace ManagerService.Tests.Controllers
{
Id = "p1", Name = "Standard",
StorageQuotaBytes = 10_000_000_000L,
AiRequestsPerMonth = 100
AiTokensPerMonth = 100
});
db.Instances.Add(new Instance
{
@ -154,7 +154,7 @@ namespace ManagerService.Tests.Controllers
var ok = Assert.IsType<OkObjectResult>(result);
var dto = Assert.IsType<InstanceQuotaDTO>(ok.Value);
Assert.Equal(10_000_000_000L, dto.storageQuotaBytes);
Assert.Equal(100, dto.aiRequestsPerMonth);
Assert.Equal(100, dto.aiTokensPerMonth);
}
[Fact]
@ -184,7 +184,7 @@ namespace ManagerService.Tests.Controllers
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow,
AiRequestsThisMonth = 7, AiUsageMonthKey = monthKey
AiTokensThisMonth = 7, AiUsageMonthKey = monthKey
});
db.SaveChanges();
@ -192,7 +192,7 @@ namespace ManagerService.Tests.Controllers
var ok = Assert.IsType<OkObjectResult>(result);
var dto = Assert.IsType<InstanceQuotaDTO>(ok.Value);
Assert.Equal(7, dto.aiRequestsUsed);
Assert.Equal(7, dto.aiTokensUsed);
}
[Fact]
@ -202,7 +202,7 @@ namespace ManagerService.Tests.Controllers
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow,
AiRequestsThisMonth = 7, AiUsageMonthKey = "2020-01"
AiTokensThisMonth = 7, AiUsageMonthKey = "2020-01"
});
db.SaveChanges();
@ -210,7 +210,7 @@ namespace ManagerService.Tests.Controllers
var ok = Assert.IsType<OkObjectResult>(result);
var dto = Assert.IsType<InstanceQuotaDTO>(ok.Value);
Assert.Equal(0, dto.aiRequestsUsed);
Assert.Equal(0, dto.aiTokensUsed);
}
// ── DELETE ───────────────────────────────────────────────────────────

View File

@ -38,7 +38,7 @@ namespace ManagerService.Tests.Controllers
using var db = DbContextFactory.Create();
db.SubscriptionPlans.Add(new SubscriptionPlan
{
Id = "p1", Name = "Starter", StorageQuotaBytes = 1024, AiRequestsPerMonth = 50
Id = "p1", Name = "Starter", StorageQuotaBytes = 1024, AiTokensPerMonth = 50
});
db.SaveChanges();
@ -48,7 +48,7 @@ namespace ManagerService.Tests.Controllers
var dto = Assert.IsType<SubscriptionPlanDTO>(ok.Value);
Assert.Equal("Starter", dto.name);
Assert.Equal(1024, dto.storageQuotaBytes);
Assert.Equal(50, dto.aiRequestsPerMonth);
Assert.Equal(50, dto.aiTokensPerMonth);
}
[Fact]
@ -67,7 +67,7 @@ namespace ManagerService.Tests.Controllers
public void Create_ValidDto_PersistsAndReturnsDto()
{
using var db = DbContextFactory.Create();
var dto = new SubscriptionPlanDTO { name = "Premium", storageQuotaBytes = 5000, aiRequestsPerMonth = 100 };
var dto = new SubscriptionPlanDTO { name = "Premium", storageQuotaBytes = 5000, aiTokensPerMonth = 100 };
var result = BuildController(db).Create(dto);
@ -97,7 +97,7 @@ namespace ManagerService.Tests.Controllers
db.SubscriptionPlans.Add(new SubscriptionPlan { Id = "p1", Name = "Old" });
db.SaveChanges();
var result = BuildController(db).Update(new SubscriptionPlanDTO { id = "p1", name = "New", storageQuotaBytes = 0, aiRequestsPerMonth = 0 });
var result = BuildController(db).Update(new SubscriptionPlanDTO { id = "p1", name = "New", storageQuotaBytes = 0, aiTokensPerMonth = 0 });
Assert.IsType<OkObjectResult>(result);
Assert.Equal("New", db.SubscriptionPlans.First().Name);

View File

@ -58,18 +58,20 @@ namespace ManagerService.Controllers
var monthKey = DateTime.UtcNow.ToString("yyyy-MM");
if (instance.AiUsageMonthKey != monthKey)
{
instance.AiRequestsThisMonth = 0;
instance.AiTokensThisMonth = 0;
instance.AiUsageMonthKey = monthKey;
_context.SaveChanges();
}
var quota = instance.AiRequestsPerMonth;
if (quota > 0 && instance.AiRequestsThisMonth >= quota)
var quota = instance.AiTokensPerMonth;
if (quota > 0 && instance.AiTokensThisMonth >= quota)
return StatusCode(429, "Quota IA mensuel dépassé");
instance.AiRequestsThisMonth++;
var result = await _assistantService.TranslateAsync(request);
instance.AiTokensThisMonth += result.TokensUsed;
_context.SaveChanges();
var result = await _assistantService.TranslateAsync(request);
return Ok(result);
}
catch (Exception ex)
@ -110,18 +112,20 @@ namespace ManagerService.Controllers
var monthKey = DateTime.UtcNow.ToString("yyyy-MM");
if (instance.AiUsageMonthKey != monthKey)
{
instance.AiRequestsThisMonth = 0;
instance.AiTokensThisMonth = 0;
instance.AiUsageMonthKey = monthKey;
_context.SaveChanges();
}
var quota = instance.AiRequestsPerMonth;
if (quota > 0 && instance.AiRequestsThisMonth >= quota)
var quota = instance.AiTokensPerMonth;
if (quota > 0 && instance.AiTokensThisMonth >= quota)
return StatusCode(429, "Quota IA mensuel dépassé");
instance.AiRequestsThisMonth++;
var result = await _assistantService.ChatAsync(request);
instance.AiTokensThisMonth += result.TokensUsed;
_context.SaveChanges();
var result = await _assistantService.ChatAsync(request);
return Ok(result);
}
catch (Exception ex)

View File

@ -49,7 +49,7 @@ namespace ManagerService.Controllers
/// Get a list of all configuration (summary)
/// </summary>
/// <param name="id">id instance</param>
[Authorize(Policy = ManagerService.Service.Security.Policies.AppReadAccess)]
[AllowAnonymous]
[ProducesResponseType(typeof(List<ConfigurationDTO>), 200)]
[ProducesResponseType(typeof(string), 500)]
[HttpGet]
@ -114,7 +114,7 @@ namespace ManagerService.Controllers
/// Get a specific display configuration
/// </summary>
/// <param name="id">id configuration</param>
[Authorize(Policy = ManagerService.Service.Security.Policies.AppReadAccess)]
[AllowAnonymous]
[ProducesResponseType(typeof(ConfigurationDTO), 200)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]

View File

@ -123,7 +123,7 @@ namespace ManagerService.Controllers
if (plan != null)
{
instance.StorageQuotaBytes = plan.StorageQuotaBytes;
instance.AiRequestsPerMonth = plan.AiRequestsPerMonth;
instance.AiTokensPerMonth = plan.AiTokensPerMonth;
instance.HasStats = plan.HasStats;
instance.StatsHistoryDays = plan.StatsHistoryDays;
instance.HasAdvancedStats = plan.HasAdvancedStats;
@ -338,10 +338,10 @@ namespace ManagerService.Controllers
.Sum(r => (long?)r.SizeBytes) ?? 0;
var monthKey = DateTime.UtcNow.ToString("yyyy-MM");
var aiUsed = instance.AiUsageMonthKey == monthKey ? instance.AiRequestsThisMonth : 0;
var aiUsed = instance.AiUsageMonthKey == monthKey ? instance.AiTokensThisMonth : 0;
var storageQuota = instance.StorageQuotaBytes;
var aiQuota = instance.AiRequestsPerMonth;
var aiQuota = instance.AiTokensPerMonth;
if ((storageQuota == 0 || aiQuota == 0) && instance.SubscriptionPlanId != null)
{
@ -349,7 +349,7 @@ namespace ManagerService.Controllers
if (plan != null)
{
if (storageQuota == 0) storageQuota = plan.StorageQuotaBytes;
if (aiQuota == 0) aiQuota = plan.AiRequestsPerMonth;
if (aiQuota == 0) aiQuota = plan.AiTokensPerMonth;
}
}
@ -357,8 +357,8 @@ namespace ManagerService.Controllers
{
storageUsedBytes = storageUsed,
storageQuotaBytes = storageQuota,
aiRequestsUsed = aiUsed,
aiRequestsPerMonth = aiQuota
aiTokensUsed = aiUsed,
aiTokensPerMonth = aiQuota
});
}
catch (Exception ex)

View File

@ -851,7 +851,6 @@ namespace ManagerService.Controllers
[HttpPut("order")]
public ObjectResult UpdateOrder([FromBody] List<SectionDTO> updatedSectionsOrder)
{
// TODO REWRITE LOGIC..
try
{
if (updatedSectionsOrder == null)
@ -864,16 +863,42 @@ namespace ManagerService.Controllers
throw new KeyNotFoundException($"Section {section.label} with id {section.id} does not exist");
}
foreach (var updatedSection in updatedSectionsOrder)
// The client may only send a subset of the configuration's sections (e.g. a filtered
// reorder view) — renormalize against the full top-level set so `Order` always stays
// 0-based and contiguous, instead of blindly writing the payload's values.
foreach (var configurationId in updatedSectionsOrder.Select(s => s.configurationId).Distinct())
{
var section = _myInfoMateDbContext.Sections.FirstOrDefault(s => s.Id == updatedSection.id);
//OldSection section = _sectionService.GetById(updatedSection.id);
section.Order = updatedSection.order.GetValueOrDefault();
_myInfoMateDbContext.SaveChanges();
var baseline = _myInfoMateDbContext.Sections
.Where(s => s.ConfigurationId == configurationId && !s.IsSubSection)
.OrderBy(s => s.Order)
.ToList();
//_sectionService.Update(section.Id, section);
var includedIds = updatedSectionsOrder
.Where(s => s.configurationId == configurationId)
.Select(s => s.id)
.ToHashSet();
var newSubsequence = updatedSectionsOrder
.Where(s => s.configurationId == configurationId)
.OrderBy(s => s.order.GetValueOrDefault())
.Select(s => baseline.First(b => b.Id == s.id))
.ToList();
var slots = baseline
.Select((section, index) => (section, index))
.Where(t => includedIds.Contains(t.section.Id))
.Select(t => t.index)
.ToList();
for (int k = 0; k < slots.Count; k++)
baseline[slots[k]] = newSubsequence[k];
for (int i = 0; i < baseline.Count; i++)
baseline[i].Order = i;
}
_myInfoMateDbContext.SaveChanges();
if (updatedSectionsOrder.Count > 0) {
MqttClientService.PublishMessage($"config/{updatedSectionsOrder[0].configurationId}", JsonConvert.SerializeObject(new PlayerMessageDTO() { configChanged = true }));
}
@ -975,7 +1000,7 @@ namespace ManagerService.Controllers
var configuration = _myInfoMateDbContext.Configurations.FirstOrDefault(c => c.Id == section.ConfigurationId);
List<Section> sections = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == section.ConfigurationId && !s.IsSubSection).ToList();
int i = 1;
int i = 0;
List<Section> orderedSection = sections.OrderBy(s => s.Order).ToList();
foreach (var sectionDb in orderedSection)
{

View File

@ -50,6 +50,7 @@ namespace ManagerService.DTOs
/// Le front peut désactiver l'écoute automatique après le TTS.
/// </summary>
public bool ExpectsReply { get; set; } = true;
public long TokensUsed { get; set; }
}
public class AiTranslateRequest
@ -62,5 +63,6 @@ namespace ManagerService.DTOs
public class AiTranslateResponse
{
public Dictionary<string, string> Translations { get; set; } = new();
public long TokensUsed { get; set; }
}
}

View File

@ -23,10 +23,10 @@ namespace ManagerService.DTOs
public string? subscriptionPlanId { get; set; }
public SubscriptionPlanDTO? subscriptionPlan { get; set; }
public int? aiRequestsThisMonth { get; set; }
public long? aiTokensThisMonth { get; set; }
public string? aiUsageMonthKey { get; set; }
public long? storageQuotaBytes { get; set; }
public int? aiRequestsPerMonth { get; set; }
public long? aiTokensPerMonth { get; set; }
public bool? hasStats { get; set; }
public int? statsHistoryDays { get; set; }
public bool? hasAdvancedStats { get; set; }

View File

@ -4,7 +4,7 @@ namespace ManagerService.DTOs
{
public long storageUsedBytes { get; set; }
public long storageQuotaBytes { get; set; }
public int aiRequestsUsed { get; set; }
public int aiRequestsPerMonth { get; set; }
public long aiTokensUsed { get; set; }
public long aiTokensPerMonth { get; set; }
}
}

View File

@ -5,7 +5,7 @@ namespace ManagerService.DTOs
public string? id { get; set; }
public string name { get; set; }
public long storageQuotaBytes { get; set; }
public int aiRequestsPerMonth { get; set; }
public long aiTokensPerMonth { get; set; }
public bool hasStats { get; set; }
public int statsHistoryDays { get; set; }
public bool hasAdvancedStats { get; set; }

View File

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
namespace ManagerService.Data
{
public class ApiKey
public class ApiKey : IAuditableEntity
{
[Key]
public string Id { get; set; }
@ -28,6 +28,8 @@ namespace ManagerService.Data
public DateTime DateCreation { get; set; }
public DateTime DateUpdate { get; set; }
public DateTime? DateExpiration { get; set; }
}
}

View File

@ -13,11 +13,15 @@ namespace ManagerService.Data
/// application instances, optionally with an order or active status.
/// Useful for managing configuration assignment and display logic per app type.
/// </summary>
public class AppConfigurationLink
public class AppConfigurationLink : IAuditableEntity
{
[Key]
public string Id { get; set; }
public DateTime DateCreation { get; set; }
public DateTime DateUpdate { get; set; }
[Required]
public string ConfigurationId { get; set; }

View File

@ -2,10 +2,12 @@
using ManagerService.Data.SubSection;
using ManagerService.DTOs;
using ManagerService.Services;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Microsoft.EntityFrameworkCore;
namespace ManagerService.Data
{
@ -13,7 +15,8 @@ namespace ManagerService.Data
/// Defines the link between an application instance and a configuration,
/// allowing apps to use one or multiple configurations.
/// </summary>
public class ApplicationInstance
[Index(nameof(InstanceId))]
public class ApplicationInstance : IAuditableEntity
{
[Key]
public string Id { get; set; }
@ -21,6 +24,10 @@ namespace ManagerService.Data
[Required]
public string InstanceId { get; set; }
public DateTime DateCreation { get; set; }
public DateTime DateUpdate { get; set; }
[Required]
public AppType AppType { get; set; }

View File

@ -1,7 +1,9 @@
using System;
using Microsoft.EntityFrameworkCore;
namespace ManagerService.Data
{
[Index(nameof(InstanceId))]
public class AuditLog
{
public string Id { get; set; } = Guid.NewGuid().ToString();

View File

@ -5,13 +5,15 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using Microsoft.EntityFrameworkCore;
namespace ManagerService.Data
{
/// <summary>
/// Configuration Information
/// </summary>
public class Configuration
[Index(nameof(InstanceId))]
public class Configuration : IAuditableEntity
{
[Key]
[Required]
@ -39,6 +41,8 @@ namespace ManagerService.Data
public DateTime DateCreation { get; set; }
public DateTime DateUpdate { get; set; }
public bool IsOffline { get; set; }
public string LoaderImageId { get; set; } // Config can have their specific loader if needed

View File

@ -2,13 +2,15 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace ManagerService.Data
{
/// <summary>
/// Device Information (Tablet)
/// </summary>
public class Device
[Index(nameof(InstanceId))]
public class Device : IAuditableEntity
{
[Key]
[Required]

View File

@ -0,0 +1,10 @@
using System;
namespace ManagerService.Data
{
public interface IAuditableEntity
{
DateTime DateCreation { get; set; }
DateTime DateUpdate { get; set; }
}
}

View File

@ -9,7 +9,7 @@ namespace ManagerService.Data
/// <summary>
/// Instance Information
/// </summary>
public class Instance
public class Instance : IAuditableEntity
{
[Key]
[Required]
@ -20,6 +20,8 @@ namespace ManagerService.Data
public DateTime DateCreation { get; set; }
public DateTime DateUpdate { get; set; }
public string PinCode { get; set; }
public bool IsPushNotification { get; set; }
@ -43,13 +45,13 @@ namespace ManagerService.Data
[ForeignKey("SubscriptionPlanId")]
public SubscriptionPlan? SubscriptionPlan { get; set; }
public int AiRequestsThisMonth { get; set; } = 0;
public long AiTokensThisMonth { get; set; } = 0;
public string AiUsageMonthKey { get; set; } = "";
public long StorageQuotaBytes { get; set; } = 0;
public int AiRequestsPerMonth { get; set; } = 0;
public long AiTokensPerMonth { get; set; } = 0;
public bool HasStats { get; set; } = false;
@ -76,10 +78,10 @@ namespace ManagerService.Data
publicApiKey = PublicApiKey,
subscriptionPlanId = SubscriptionPlanId,
subscriptionPlan = SubscriptionPlan?.ToDTO(),
aiRequestsThisMonth = AiRequestsThisMonth,
aiTokensThisMonth = AiTokensThisMonth,
aiUsageMonthKey = AiUsageMonthKey,
storageQuotaBytes = StorageQuotaBytes,
aiRequestsPerMonth = AiRequestsPerMonth,
aiTokensPerMonth = AiTokensPerMonth,
hasStats = HasStats,
statsHistoryDays = StatsHistoryDays,
hasAdvancedStats = HasAdvancedStats,
@ -106,8 +108,8 @@ namespace ManagerService.Data
SubscriptionPlanId = instanceDTO.subscriptionPlanId;
if (instanceDTO.storageQuotaBytes.HasValue)
StorageQuotaBytes = instanceDTO.storageQuotaBytes.Value;
if (instanceDTO.aiRequestsPerMonth.HasValue)
AiRequestsPerMonth = instanceDTO.aiRequestsPerMonth.Value;
if (instanceDTO.aiTokensPerMonth.HasValue)
AiTokensPerMonth = instanceDTO.aiTokensPerMonth.Value;
if (instanceDTO.hasStats.HasValue)
HasStats = instanceDTO.hasStats.Value;
if (instanceDTO.statsHistoryDays.HasValue)

View File

@ -64,8 +64,19 @@ namespace ManagerService.Data
// Audit
public DbSet<AuditLog> AuditLogs { get; set; }
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
StampAuditableEntities();
var auditEntries = BuildAuditEntries();
var result = base.SaveChanges(acceptAllChangesOnSuccess);
if (auditEntries.Any())
base.SaveChanges(acceptAllChangesOnSuccess);
return result;
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
StampAuditableEntities();
var auditEntries = BuildAuditEntries();
var result = await base.SaveChangesAsync(cancellationToken);
if (auditEntries.Any())
@ -73,6 +84,18 @@ namespace ManagerService.Data
return result;
}
private void StampAuditableEntities()
{
var now = DateTime.UtcNow;
foreach (var entry in ChangeTracker.Entries<IAuditableEntity>())
{
if (entry.State == EntityState.Added)
entry.Entity.DateCreation = now;
if (entry.State is EntityState.Added or EntityState.Modified)
entry.Entity.DateUpdate = now;
}
}
private static readonly HashSet<Type> AuditedTypes = new()
{
typeof(Section), typeof(Resource), typeof(Configuration),
@ -399,7 +422,7 @@ namespace ManagerService.Data
modelBuilder.Entity<Instance>()
.Property(i => i.StorageQuotaBytes).ValueGeneratedNever();
modelBuilder.Entity<Instance>()
.Property(i => i.AiRequestsPerMonth).ValueGeneratedNever();
.Property(i => i.AiTokensPerMonth).ValueGeneratedNever();
modelBuilder.Entity<Instance>()
.Property(i => i.HasStats).ValueGeneratedNever();
modelBuilder.Entity<Instance>()
@ -414,7 +437,7 @@ namespace ManagerService.Data
Id = "plan-starter",
Name = "Starter",
StorageQuotaBytes = 1L * 1024 * 1024 * 1024, // 1 GB
AiRequestsPerMonth = 0,
AiTokensPerMonth = 0,
HasStats = false,
StatsHistoryDays = 30,
HasAdvancedStats = false,
@ -424,7 +447,7 @@ namespace ManagerService.Data
Id = "plan-standard",
Name = "Standard",
StorageQuotaBytes = 10L * 1024 * 1024 * 1024, // 10 GB
AiRequestsPerMonth = 500,
AiTokensPerMonth = 5_000_000, // ~500 req/mois * ~10k tokens/req estimés, à ajuster selon l'usage réel observé
HasStats = true,
StatsHistoryDays = 30,
HasAdvancedStats = false,
@ -434,7 +457,7 @@ namespace ManagerService.Data
Id = "plan-premium",
Name = "Premium",
StorageQuotaBytes = 50L * 1024 * 1024 * 1024, // 50 GB
AiRequestsPerMonth = 2000,
AiTokensPerMonth = 20_000_000, // ~2000 req/mois * ~10k tokens/req estimés, à ajuster selon l'usage réel observé
HasStats = true,
StatsHistoryDays = 0,
HasAdvancedStats = true,

View File

@ -1,13 +1,15 @@
using ManagerService.DTOs;
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace ManagerService.Data
{
/// <summary>
/// Resource Information
/// </summary>
public class Resource
[Index(nameof(InstanceId))]
public class Resource : IAuditableEntity
{
[Key]
[Required]
@ -28,6 +30,8 @@ namespace ManagerService.Data
/*[BsonElement("DateCreation")]*/
public DateTime DateCreation { get; set; }
public DateTime DateUpdate { get; set; }
/*[BsonElement("InstanceId")]
[BsonRequired]*/
[Required]

View File

@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace ManagerService.Data
@ -10,7 +11,8 @@ namespace ManagerService.Data
/// <summary>
/// Section Information
/// </summary>
public class Section
[Index(nameof(InstanceId))]
public class Section : IAuditableEntity
{
[Key]
[Required]
@ -45,6 +47,8 @@ namespace ManagerService.Data
public DateTime DateCreation { get; set; }
public DateTime DateUpdate { get; set; }
[Required]
public string InstanceId { get; set; }

View File

@ -1,13 +1,16 @@
using ManagerService.Data;
using ManagerService.DTOs;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.EntityFrameworkCore;
namespace ManagerService.Data.SubSection
{
public class GuidedPath // Parcours
[Index(nameof(InstanceId))]
public class GuidedPath : IAuditableEntity // Parcours
{
[Key]
public string Id { get; set; }
@ -15,6 +18,10 @@ namespace ManagerService.Data.SubSection
[Required]
public string InstanceId { get; set; }
public DateTime DateCreation { get; set; }
public DateTime DateUpdate { get; set; }
[Required]
[Column(TypeName = "jsonb")]
public List<TranslationDTO> Title { get; set; }

View File

@ -14,7 +14,7 @@ namespace ManagerService.Data
public long StorageQuotaBytes { get; set; } = 0;
public int AiRequestsPerMonth { get; set; } = 0;
public long AiTokensPerMonth { get; set; } = 0;
/// <summary>Whether the plan includes any stats at all.</summary>
public bool HasStats { get; set; } = false;
@ -32,7 +32,7 @@ namespace ManagerService.Data
id = Id,
name = Name,
storageQuotaBytes = StorageQuotaBytes,
aiRequestsPerMonth = AiRequestsPerMonth,
aiTokensPerMonth = AiTokensPerMonth,
hasStats = HasStats,
statsHistoryDays = StatsHistoryDays,
hasAdvancedStats = HasAdvancedStats,
@ -43,7 +43,7 @@ namespace ManagerService.Data
{
Name = dto.name;
StorageQuotaBytes = dto.storageQuotaBytes;
AiRequestsPerMonth = dto.aiRequestsPerMonth;
AiTokensPerMonth = dto.aiTokensPerMonth;
HasStats = dto.hasStats;
StatsHistoryDays = dto.statsHistoryDays;
HasAdvancedStats = dto.hasAdvancedStats;

View File

@ -1,13 +1,15 @@
using ManagerService.DTOs;
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace ManagerService.Data
{
/// <summary>
/// User Information
/// </summary>
public class User
[Index(nameof(InstanceId))]
public class User : IAuditableEntity
{
[Key]
[Required]
@ -42,6 +44,8 @@ namespace ManagerService.Data
//[BsonElement("DateCreation")]
public DateTime DateCreation { get; set; }
public DateTime DateUpdate { get; set; }
/*[BsonElement("InstanceId")]
[BsonRequired]*/
[Required]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,90 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ManagerService.Migrations
{
/// <inheritdoc />
public partial class AddInstanceIdIndexes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_Users_InstanceId",
table: "Users",
column: "InstanceId");
migrationBuilder.CreateIndex(
name: "IX_Sections_InstanceId",
table: "Sections",
column: "InstanceId");
migrationBuilder.CreateIndex(
name: "IX_Resources_InstanceId",
table: "Resources",
column: "InstanceId");
migrationBuilder.CreateIndex(
name: "IX_GuidedPaths_InstanceId",
table: "GuidedPaths",
column: "InstanceId");
migrationBuilder.CreateIndex(
name: "IX_Devices_InstanceId",
table: "Devices",
column: "InstanceId");
migrationBuilder.CreateIndex(
name: "IX_Configurations_InstanceId",
table: "Configurations",
column: "InstanceId");
migrationBuilder.CreateIndex(
name: "IX_AuditLogs_InstanceId",
table: "AuditLogs",
column: "InstanceId");
migrationBuilder.CreateIndex(
name: "IX_ApplicationInstances_InstanceId",
table: "ApplicationInstances",
column: "InstanceId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Users_InstanceId",
table: "Users");
migrationBuilder.DropIndex(
name: "IX_Sections_InstanceId",
table: "Sections");
migrationBuilder.DropIndex(
name: "IX_Resources_InstanceId",
table: "Resources");
migrationBuilder.DropIndex(
name: "IX_GuidedPaths_InstanceId",
table: "GuidedPaths");
migrationBuilder.DropIndex(
name: "IX_Devices_InstanceId",
table: "Devices");
migrationBuilder.DropIndex(
name: "IX_Configurations_InstanceId",
table: "Configurations");
migrationBuilder.DropIndex(
name: "IX_AuditLogs_InstanceId",
table: "AuditLogs");
migrationBuilder.DropIndex(
name: "IX_ApplicationInstances_InstanceId",
table: "ApplicationInstances");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,165 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ManagerService.Migrations
{
/// <inheritdoc />
public partial class AddDateUpdateAudit : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdate",
table: "Users",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdate",
table: "Sections",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdate",
table: "Resources",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdate",
table: "Instances",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "DateCreation",
table: "GuidedPaths",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdate",
table: "GuidedPaths",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdate",
table: "Configurations",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "DateCreation",
table: "ApplicationInstances",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdate",
table: "ApplicationInstances",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "DateCreation",
table: "AppConfigurationLinks",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdate",
table: "AppConfigurationLinks",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "DateUpdate",
table: "ApiKeys",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
// Backfill : pour les lignes existantes, DateUpdate reprend la DateCreation connue.
migrationBuilder.Sql("UPDATE \"Users\" SET \"DateUpdate\" = \"DateCreation\";");
migrationBuilder.Sql("UPDATE \"Sections\" SET \"DateUpdate\" = \"DateCreation\";");
migrationBuilder.Sql("UPDATE \"Resources\" SET \"DateUpdate\" = \"DateCreation\";");
migrationBuilder.Sql("UPDATE \"Instances\" SET \"DateUpdate\" = \"DateCreation\";");
migrationBuilder.Sql("UPDATE \"Configurations\" SET \"DateUpdate\" = \"DateCreation\";");
migrationBuilder.Sql("UPDATE \"ApiKeys\" SET \"DateUpdate\" = \"DateCreation\";");
// GuidedPaths/ApplicationInstances/AppConfigurationLinks n'avaient aucune date avant cette migration :
// on ne peut pas reconstituer la vraie date de création, donc on initialise les deux au moment du backfill.
migrationBuilder.Sql("UPDATE \"GuidedPaths\" SET \"DateCreation\" = now(), \"DateUpdate\" = now();");
migrationBuilder.Sql("UPDATE \"ApplicationInstances\" SET \"DateCreation\" = now(), \"DateUpdate\" = now();");
migrationBuilder.Sql("UPDATE \"AppConfigurationLinks\" SET \"DateCreation\" = now(), \"DateUpdate\" = now();");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DateUpdate",
table: "Users");
migrationBuilder.DropColumn(
name: "DateUpdate",
table: "Sections");
migrationBuilder.DropColumn(
name: "DateUpdate",
table: "Resources");
migrationBuilder.DropColumn(
name: "DateUpdate",
table: "Instances");
migrationBuilder.DropColumn(
name: "DateCreation",
table: "GuidedPaths");
migrationBuilder.DropColumn(
name: "DateUpdate",
table: "GuidedPaths");
migrationBuilder.DropColumn(
name: "DateUpdate",
table: "Configurations");
migrationBuilder.DropColumn(
name: "DateCreation",
table: "ApplicationInstances");
migrationBuilder.DropColumn(
name: "DateUpdate",
table: "ApplicationInstances");
migrationBuilder.DropColumn(
name: "DateCreation",
table: "AppConfigurationLinks");
migrationBuilder.DropColumn(
name: "DateUpdate",
table: "AppConfigurationLinks");
migrationBuilder.DropColumn(
name: "DateUpdate",
table: "ApiKeys");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,126 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ManagerService.Migrations
{
/// <inheritdoc />
public partial class AiQuotaTokensInsteadOfRequests : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AiRequestsPerMonth",
table: "SubscriptionPlans");
migrationBuilder.DropColumn(
name: "AiRequestsPerMonth",
table: "Instances");
migrationBuilder.DropColumn(
name: "AiRequestsThisMonth",
table: "Instances");
migrationBuilder.AddColumn<long>(
name: "AiTokensPerMonth",
table: "SubscriptionPlans",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.AddColumn<long>(
name: "AiTokensPerMonth",
table: "Instances",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.AddColumn<long>(
name: "AiTokensThisMonth",
table: "Instances",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-premium",
column: "AiTokensPerMonth",
value: 20000000L);
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-standard",
column: "AiTokensPerMonth",
value: 5000000L);
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-starter",
column: "AiTokensPerMonth",
value: 0L);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AiTokensPerMonth",
table: "SubscriptionPlans");
migrationBuilder.DropColumn(
name: "AiTokensPerMonth",
table: "Instances");
migrationBuilder.DropColumn(
name: "AiTokensThisMonth",
table: "Instances");
migrationBuilder.AddColumn<int>(
name: "AiRequestsPerMonth",
table: "SubscriptionPlans",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "AiRequestsPerMonth",
table: "Instances",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "AiRequestsThisMonth",
table: "Instances",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-premium",
column: "AiRequestsPerMonth",
value: 2000);
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-standard",
column: "AiRequestsPerMonth",
value: 500);
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-starter",
column: "AiRequestsPerMonth",
value: 0);
}
}
}

View File

@ -41,6 +41,9 @@ namespace ManagerService.Migrations
b.Property<DateTime?>("DateExpiration")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateUpdate")
.HasColumnType("timestamp with time zone");
b.Property<string>("InstanceId")
.IsRequired()
.HasColumnType("text");
@ -78,6 +81,12 @@ namespace ManagerService.Migrations
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("DateCreation")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateUpdate")
.HasColumnType("timestamp with time zone");
b.Property<string>("DeviceId")
.HasColumnType("text");
@ -139,6 +148,12 @@ namespace ManagerService.Migrations
b.Property<int>("AppType")
.HasColumnType("integer");
b.Property<DateTime>("DateCreation")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateUpdate")
.HasColumnType("timestamp with time zone");
b.Property<string>("InstanceId")
.IsRequired()
.HasColumnType("text");
@ -172,6 +187,8 @@ namespace ManagerService.Migrations
b.HasKey("Id");
b.HasIndex("InstanceId");
b.HasIndex("SectionEventId");
b.ToTable("ApplicationInstances");
@ -208,6 +225,8 @@ namespace ManagerService.Migrations
b.HasKey("Id");
b.HasIndex("InstanceId");
b.ToTable("AuditLogs");
});
@ -219,6 +238,9 @@ namespace ManagerService.Migrations
b.Property<DateTime>("DateCreation")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateUpdate")
.HasColumnType("timestamp with time zone");
b.Property<string>("ImageId")
.HasColumnType("text");
@ -266,6 +288,8 @@ namespace ManagerService.Migrations
b.HasKey("Id");
b.HasIndex("InstanceId");
b.ToTable("Configurations");
});
@ -325,6 +349,8 @@ namespace ManagerService.Migrations
b.HasIndex("ConfigurationId");
b.HasIndex("InstanceId");
b.ToTable("Devices");
});
@ -333,11 +359,11 @@ namespace ManagerService.Migrations
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AiRequestsPerMonth")
.HasColumnType("integer");
b.Property<long>("AiTokensPerMonth")
.HasColumnType("bigint");
b.Property<int>("AiRequestsThisMonth")
.HasColumnType("integer");
b.Property<long>("AiTokensThisMonth")
.HasColumnType("bigint");
b.Property<string>("AiUsageMonthKey")
.HasColumnType("text");
@ -345,6 +371,9 @@ namespace ManagerService.Migrations
b.Property<DateTime>("DateCreation")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateUpdate")
.HasColumnType("timestamp with time zone");
b.Property<bool>("HasAdvancedStats")
.HasColumnType("boolean");
@ -449,6 +478,9 @@ namespace ManagerService.Migrations
b.Property<DateTime>("DateCreation")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateUpdate")
.HasColumnType("timestamp with time zone");
b.Property<string>("InstanceId")
.IsRequired()
.HasColumnType("text");
@ -468,6 +500,8 @@ namespace ManagerService.Migrations
b.HasKey("Id");
b.HasIndex("InstanceId");
b.ToTable("Resources");
});
@ -486,6 +520,9 @@ namespace ManagerService.Migrations
b.Property<DateTime>("DateCreation")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateUpdate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("jsonb");
@ -544,6 +581,8 @@ namespace ManagerService.Migrations
b.HasKey("Id");
b.HasIndex("InstanceId");
b.HasIndex("SectionMenuId");
b.ToTable("Sections");
@ -705,6 +744,12 @@ namespace ManagerService.Migrations
b.Property<string>("Id")
.HasColumnType("text");
b.Property<DateTime>("DateCreation")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateUpdate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("jsonb");
@ -753,6 +798,8 @@ namespace ManagerService.Migrations
b.HasIndex("ImageResourceId");
b.HasIndex("InstanceId");
b.HasIndex("SectionEventId");
b.HasIndex("SectionParcoursId");
@ -953,8 +1000,8 @@ namespace ManagerService.Migrations
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AiRequestsPerMonth")
.HasColumnType("integer");
b.Property<long>("AiTokensPerMonth")
.HasColumnType("bigint");
b.Property<bool>("HasAdvancedStats")
.HasColumnType("boolean");
@ -980,7 +1027,7 @@ namespace ManagerService.Migrations
new
{
Id = "plan-starter",
AiRequestsPerMonth = 0,
AiTokensPerMonth = 0L,
HasAdvancedStats = false,
HasStats = false,
Name = "Starter",
@ -990,7 +1037,7 @@ namespace ManagerService.Migrations
new
{
Id = "plan-standard",
AiRequestsPerMonth = 500,
AiTokensPerMonth = 5000000L,
HasAdvancedStats = false,
HasStats = true,
Name = "Standard",
@ -1000,7 +1047,7 @@ namespace ManagerService.Migrations
new
{
Id = "plan-premium",
AiRequestsPerMonth = 2000,
AiTokensPerMonth = 20000000L,
HasAdvancedStats = true,
HasStats = true,
Name = "Premium",
@ -1017,6 +1064,9 @@ namespace ManagerService.Migrations
b.Property<DateTime>("DateCreation")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateUpdate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
@ -1045,6 +1095,8 @@ namespace ManagerService.Migrations
b.HasKey("Id");
b.HasIndex("InstanceId");
b.ToTable("Users");
});

View File

@ -57,7 +57,7 @@ namespace ManagerService.Services
}
var translations = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(json) ?? new();
return new AiTranslateResponse { Translations = translations };
return new AiTranslateResponse { Translations = translations, TokensUsed = response.Usage?.TotalTokenCount ?? 0 };
}
private static string StripHtml(string? html) =>
@ -493,7 +493,7 @@ namespace ManagerService.Services
reply = response.Text ?? "";
expectsReply = true;
}
return new AiChatResponse { Reply = reply, Cards = cards, Navigation = navigation, ExpectsReply = expectsReply };
return new AiChatResponse { Reply = reply, Cards = cards, Navigation = navigation, ExpectsReply = expectsReply, TokensUsed = response.Usage?.TotalTokenCount ?? 0 };
}
catch (System.ClientModel.ClientResultException ex)
{
@ -789,7 +789,7 @@ namespace ManagerService.Services
reply = response.Text ?? "";
expectsReply = true;
}
return new AiChatResponse { Reply = reply, Navigation = navigation, ExpectsReply = expectsReply };
return new AiChatResponse { Reply = reply, Navigation = navigation, ExpectsReply = expectsReply, TokensUsed = response.Usage?.TotalTokenCount ?? 0 };
}
catch (System.ClientModel.ClientResultException ex)
{