save progress
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -83,6 +84,11 @@ public sealed class LlmInferenceCacheOptions
|
||||
/// </summary>
|
||||
public int MaxContentLength { get; set; } = 100_000;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of cache entries (0 = unlimited).
|
||||
/// </summary>
|
||||
public int MaxEntries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use sliding expiration.
|
||||
/// </summary>
|
||||
@@ -194,7 +200,8 @@ public sealed class InMemoryLlmInferenceCache : ILlmInferenceCache, IDisposable
|
||||
{
|
||||
if (_cache.TryGetValue(key, out var entry))
|
||||
{
|
||||
if (entry.ExpiresAt > _timeProvider.GetUtcNow())
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
if (entry.ExpiresAt > now)
|
||||
{
|
||||
Interlocked.Increment(ref _hits);
|
||||
Interlocked.Add(ref _tokensSaved, entry.Result.OutputTokens ?? 0);
|
||||
@@ -202,7 +209,8 @@ public sealed class InMemoryLlmInferenceCache : ILlmInferenceCache, IDisposable
|
||||
// Update access time for sliding expiration
|
||||
if (_options.SlidingExpiration)
|
||||
{
|
||||
entry.AccessedAt = _timeProvider.GetUtcNow();
|
||||
entry.AccessedAt = now;
|
||||
entry.ExpiresAt = ApplySlidingExpiration(entry, now);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Cache hit for key {Key}", key);
|
||||
@@ -246,6 +254,11 @@ public sealed class InMemoryLlmInferenceCache : ILlmInferenceCache, IDisposable
|
||||
|
||||
var key = ComputeCacheKey(request, providerId);
|
||||
var ttl = result.Deterministic ? _options.DefaultTtl : _options.ShortTtl;
|
||||
ttl = ClampTtl(ttl);
|
||||
if (ttl <= TimeSpan.Zero)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
var entry = new CacheEntry
|
||||
@@ -253,12 +266,14 @@ public sealed class InMemoryLlmInferenceCache : ILlmInferenceCache, IDisposable
|
||||
Result = result,
|
||||
CreatedAt = now,
|
||||
AccessedAt = now,
|
||||
ExpiresAt = now.Add(ttl)
|
||||
ExpiresAt = now.Add(ttl),
|
||||
Ttl = ttl
|
||||
};
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_cache[key] = entry;
|
||||
EnforceCacheLimit();
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _sets);
|
||||
@@ -328,7 +343,7 @@ public sealed class InMemoryLlmInferenceCache : ILlmInferenceCache, IDisposable
|
||||
|
||||
// Include temperature and max tokens in key
|
||||
sb.Append(':');
|
||||
sb.Append(request.Temperature.ToString("F2"));
|
||||
sb.Append(request.Temperature.ToString("F2", CultureInfo.InvariantCulture));
|
||||
sb.Append(':');
|
||||
sb.Append(request.MaxTokens);
|
||||
|
||||
@@ -372,12 +387,75 @@ public sealed class InMemoryLlmInferenceCache : ILlmInferenceCache, IDisposable
|
||||
_cleanupTimer.Dispose();
|
||||
}
|
||||
|
||||
private TimeSpan ClampTtl(TimeSpan ttl)
|
||||
{
|
||||
if (ttl <= TimeSpan.Zero)
|
||||
{
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
|
||||
if (_options.MaxTtl > TimeSpan.Zero && ttl > _options.MaxTtl)
|
||||
{
|
||||
return _options.MaxTtl;
|
||||
}
|
||||
|
||||
return ttl;
|
||||
}
|
||||
|
||||
private DateTimeOffset ApplySlidingExpiration(CacheEntry entry, DateTimeOffset now)
|
||||
{
|
||||
if (entry.Ttl <= TimeSpan.Zero)
|
||||
{
|
||||
return entry.ExpiresAt;
|
||||
}
|
||||
|
||||
var proposed = now.Add(entry.Ttl);
|
||||
if (_options.MaxTtl > TimeSpan.Zero)
|
||||
{
|
||||
var maxAllowed = entry.CreatedAt.Add(_options.MaxTtl);
|
||||
if (proposed > maxAllowed)
|
||||
{
|
||||
return maxAllowed;
|
||||
}
|
||||
}
|
||||
|
||||
return proposed;
|
||||
}
|
||||
|
||||
private void EnforceCacheLimit()
|
||||
{
|
||||
if (_options.MaxEntries <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var removeCount = _cache.Count - _options.MaxEntries;
|
||||
if (removeCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var keysToRemove = _cache
|
||||
.OrderBy(entry => entry.Value.AccessedAt)
|
||||
.ThenBy(entry => entry.Value.CreatedAt)
|
||||
.ThenBy(entry => entry.Key, StringComparer.Ordinal)
|
||||
.Take(removeCount)
|
||||
.Select(entry => entry.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
_cache.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CacheEntry
|
||||
{
|
||||
public required LlmCompletionResult Result { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset AccessedAt { get; set; }
|
||||
public DateTimeOffset ExpiresAt { get; init; }
|
||||
public DateTimeOffset ExpiresAt { get; set; }
|
||||
public TimeSpan Ttl { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Inference;
|
||||
|
||||
@@ -142,6 +144,13 @@ public sealed class SignedModelBundleManager : ISignedModelBundleManager
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
};
|
||||
|
||||
private readonly TimeProvider _clock;
|
||||
|
||||
public SignedModelBundleManager(TimeProvider? clock = null)
|
||||
{
|
||||
_clock = clock ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<SigningResult> SignBundleAsync(
|
||||
string bundlePath,
|
||||
IModelBundleSigner signer,
|
||||
@@ -166,11 +175,14 @@ public sealed class SignedModelBundleManager : ISignedModelBundleManager
|
||||
var manifestBytes = await File.ReadAllBytesAsync(manifestPath, cancellationToken);
|
||||
var manifestDigest = ComputeSha256(manifestBytes);
|
||||
|
||||
var signedAt = _clock.GetUtcNow();
|
||||
var signedAtValue = signedAt.ToString("O", CultureInfo.InvariantCulture);
|
||||
|
||||
// Create the payload (manifest digest + metadata)
|
||||
var payload = new
|
||||
{
|
||||
manifest_digest = manifestDigest,
|
||||
signed_at = DateTime.UtcNow.ToString("o"),
|
||||
signed_at = signedAtValue,
|
||||
bundle_path = Path.GetFileName(bundlePath)
|
||||
};
|
||||
var payloadJson = JsonSerializer.Serialize(payload, JsonOptions);
|
||||
@@ -182,7 +194,7 @@ public sealed class SignedModelBundleManager : ISignedModelBundleManager
|
||||
var signature = await signer.SignAsync(pae, cancellationToken);
|
||||
var signatureBase64 = Convert.ToBase64String(signature);
|
||||
|
||||
var signatureId = $"{signer.CryptoScheme}-{DateTime.UtcNow:yyyyMMddHHmmss}-{manifestDigest[..8]}";
|
||||
var signatureId = $"{signer.CryptoScheme}-{signedAt.UtcDateTime:yyyyMMddHHmmss}-{manifestDigest[..8]}";
|
||||
|
||||
// Create DSSE envelope
|
||||
var envelope = new ModelBundleSignatureEnvelope
|
||||
@@ -205,13 +217,13 @@ public sealed class SignedModelBundleManager : ISignedModelBundleManager
|
||||
await File.WriteAllTextAsync(envelopePath, envelopeJson, cancellationToken);
|
||||
|
||||
// Update manifest with signature info
|
||||
var manifest = await File.ReadAllTextAsync(manifestPath, cancellationToken);
|
||||
var manifestObj = JsonSerializer.Deserialize<Dictionary<string, object>>(manifest);
|
||||
if (manifestObj != null)
|
||||
var manifestJson = await File.ReadAllTextAsync(manifestPath, cancellationToken);
|
||||
var manifestNode = JsonNode.Parse(manifestJson);
|
||||
if (manifestNode is JsonObject manifestObject)
|
||||
{
|
||||
manifestObj["signature_id"] = signatureId;
|
||||
manifestObj["crypto_scheme"] = signer.CryptoScheme;
|
||||
var updatedManifest = JsonSerializer.Serialize(manifestObj, JsonOptions);
|
||||
manifestObject["signature_id"] = signatureId;
|
||||
manifestObject["crypto_scheme"] = signer.CryptoScheme;
|
||||
var updatedManifest = manifestObject.ToJsonString(JsonOptions);
|
||||
await File.WriteAllTextAsync(manifestPath, updatedManifest, cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
|
||||
10
src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md
Normal file
10
src/AdvisoryAI/StellaOps.AdvisoryAI/TASKS.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Advisory AI Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0017-M | DONE | Maintainability audit for StellaOps.AdvisoryAI. |
|
||||
| AUDIT-0017-T | DONE | Test coverage audit for StellaOps.AdvisoryAI. |
|
||||
| AUDIT-0017-A | DONE | Pending approval for changes. |
|
||||
Reference in New Issue
Block a user