feat: externalize AI configuration, implement resilience policies, and update extraction prompt formatting
This commit is contained in:
+7
-7
@@ -1,6 +1,6 @@
|
|||||||
# 🤖 LLM Agent Implementation Backlog: AI Semantic Integration
|
# 🤖 LLM Agent Implementation Backlog: AI Semantic Integration
|
||||||
|
|
||||||
**Project Context:** .NET 10, EF Core (SQLite), `Microsoft.Extensions.AI`.
|
**Project Context:** .NET 10, EF Core (SQLite), `Microsoft.Extensions.AI`, [`GeminiDotnet`](https://github.com/rabuckley/GeminiDotnet).
|
||||||
**Core Goal:** Integrate Gemini 1.5 Flash with a persistent Semantic Cache to minimize API costs and latency.
|
**Core Goal:** Integrate Gemini 1.5 Flash with a persistent Semantic Cache to minimize API costs and latency.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -31,22 +31,22 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 🧠 Phase 2: AI Client & Contract Definition
|
## 🧠 Phase 2: AI Client & Contract Definition
|
||||||
**Objective:** Set up the communication bridge with Google Gemini API.
|
**Objective:** Set up the communication bridge with Google Gemini API using [`GeminiDotnet`](https://github.com/rabuckley/GeminiDotnet).
|
||||||
|
|
||||||
### Task 2.1: Define Data Transfer Objects (DTOs)
|
### Task 2.1: Define Data Transfer Objects (DTOs)
|
||||||
* **Target Folder:** `Core/DTOs/AI`.
|
* **Target Folder:** `NexusReader.Application/DTOs/AI`.
|
||||||
* **Requirements:**
|
* **Requirements:**
|
||||||
* Define `KnowledgePacket` record containing `List<KeyConcept>` and `List<QuizQuestion>`.
|
* Define `KnowledgePacket` record containing `List<KeyConcept>` and `List<QuizQuestion>`.
|
||||||
* Use `[JsonPropertyName]` attributes for strict JSON mapping.
|
* Use `[JsonPropertyName]` attributes for strict JSON mapping.
|
||||||
* **LLM Instructions:** "Define immutable records for the AI response schema. Ensure they match the expected JSON structure from the system prompt."
|
* **LLM Instructions:** "Define immutable records for the AI response schema. Ensure they match the expected JSON structure from the system prompt."
|
||||||
|
|
||||||
### Task 2.2: Infrastructure AI Client Setup
|
### Task 2.2: Infrastructure AI Client Setup
|
||||||
* **Target:** `Program.cs` / Dependency Injection.
|
* **Target:** `NexusReader.Infrastructure/DependencyInjection.cs`.
|
||||||
* **Requirements:**
|
* **Requirements:**
|
||||||
* Install `Microsoft.Extensions.AI` and `Microsoft.Extensions.AI.Google`.
|
* Install `Microsoft.Extensions.AI` and `GeminiDotnet.Extensions.AI`.
|
||||||
* Register `IChatClient` using `GoogleChatClient`.
|
* Register `IChatClient` using `GeminiChatClient`.
|
||||||
* Inject `ApiKey` from `IConfiguration`.
|
* Inject `ApiKey` from `IConfiguration`.
|
||||||
* **LLM Instructions:** "Register the GoogleChatClient in the DI container. Use the .NET 10 `AddChatClient` extension pattern."
|
* **LLM Instructions:** "Register the `GeminiChatClient` in the DI container. Use the .NET 10 `AddChatClient` extension pattern."
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace NexusReader.Infrastructure.Configuration;
|
||||||
|
|
||||||
|
public class AiSettings
|
||||||
|
{
|
||||||
|
public const string SectionName = "Ai:Google";
|
||||||
|
|
||||||
|
public string ApiKey { get; set; } = string.Empty;
|
||||||
|
public string Model { get; set; } = "gemini-1.5-flash";
|
||||||
|
public int MaxInputLength { get; set; } = 15000;
|
||||||
|
public int MaxOutputTokens { get; set; } = 1000;
|
||||||
|
public int RetryAttempts { get; set; } = 3;
|
||||||
|
public double Temperature { get; set; } = 0.1;
|
||||||
|
}
|
||||||
@@ -7,6 +7,9 @@ using GeminiDotnet.Extensions.AI;
|
|||||||
using NexusReader.Infrastructure.Persistence;
|
using NexusReader.Infrastructure.Persistence;
|
||||||
using NexusReader.Application.Abstractions.Services;
|
using NexusReader.Application.Abstractions.Services;
|
||||||
using NexusReader.Infrastructure.Services;
|
using NexusReader.Infrastructure.Services;
|
||||||
|
using NexusReader.Infrastructure.Configuration;
|
||||||
|
using Polly;
|
||||||
|
using Polly.Retry;
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure;
|
namespace NexusReader.Infrastructure;
|
||||||
|
|
||||||
@@ -18,17 +21,31 @@ public static class DependencyInjection
|
|||||||
services.AddDbContext<AppDbContext>(options =>
|
services.AddDbContext<AppDbContext>(options =>
|
||||||
options.UseSqlite(connectionString));
|
options.UseSqlite(connectionString));
|
||||||
|
|
||||||
var apiKey = configuration["Ai:Google:ApiKey"];
|
services.Configure<AiSettings>(configuration.GetSection(AiSettings.SectionName));
|
||||||
if (string.IsNullOrWhiteSpace(apiKey))
|
var aiSettings = configuration.GetSection(AiSettings.SectionName).Get<AiSettings>() ?? new AiSettings();
|
||||||
{
|
|
||||||
throw new InvalidOperationException("AI Studio ApiKey is missing in configuration (Ai:Google:ApiKey).");
|
|
||||||
}
|
|
||||||
var modelId = configuration["Ai:Google:Model"] ?? "gemini-1.5-flash";
|
|
||||||
|
|
||||||
services.AddSingleton<IChatClient>(new GeminiChatClient(new GeminiClientOptions
|
if (string.IsNullOrWhiteSpace(aiSettings.ApiKey) || aiSettings.ApiKey == "PLACEHOLDER")
|
||||||
|
{
|
||||||
|
// We don't throw here to allow the app to start, but services using AI will fail gracefully
|
||||||
|
}
|
||||||
|
|
||||||
|
services.AddResiliencePipeline("ai-retry", builder =>
|
||||||
|
{
|
||||||
|
builder.AddRetry(new RetryStrategyOptions
|
||||||
|
{
|
||||||
|
ShouldHandle = new PredicateBuilder().Handle<Exception>(ex =>
|
||||||
|
ex.Message.Contains("429") || ex.Message.Contains("Too Many Requests") || ex.Message.Contains("quota")),
|
||||||
|
BackoffType = DelayBackoffType.Exponential,
|
||||||
|
UseJitter = true,
|
||||||
|
MaxRetryAttempts = aiSettings.RetryAttempts,
|
||||||
|
Delay = TimeSpan.FromSeconds(2)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddChatClient(new GeminiChatClient(new GeminiClientOptions
|
||||||
{
|
{
|
||||||
ApiKey = apiKey,
|
ApiKey = aiSettings.ApiKey,
|
||||||
ModelId = modelId
|
ModelId = aiSettings.Model
|
||||||
}));
|
}));
|
||||||
|
|
||||||
services.AddScoped<IKnowledgeService, KnowledgeService>();
|
services.AddScoped<IKnowledgeService, KnowledgeService>();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.Extensions.AI" Version="10.5.0" />
|
<PackageReference Include="Microsoft.Extensions.AI" Version="10.5.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Resilience" Version="10.5.0" />
|
||||||
<PackageReference Include="Polly" Version="8.6.6" />
|
<PackageReference Include="Polly" Version="8.6.6" />
|
||||||
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
|
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
|
||||||
<PackageReference Include="VersOne.Epub" Version="3.3.6" />
|
<PackageReference Include="VersOne.Epub" Version="3.3.6" />
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ using NexusReader.Domain.Entities;
|
|||||||
using NexusReader.Infrastructure.Helpers;
|
using NexusReader.Infrastructure.Helpers;
|
||||||
using NexusReader.Infrastructure.Persistence;
|
using NexusReader.Infrastructure.Persistence;
|
||||||
using Polly;
|
using Polly;
|
||||||
using Polly.Retry;
|
using Polly.Registry;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using NexusReader.Infrastructure.Configuration;
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Services;
|
namespace NexusReader.Infrastructure.Services;
|
||||||
|
|
||||||
@@ -16,25 +18,20 @@ public class KnowledgeService : IKnowledgeService
|
|||||||
{
|
{
|
||||||
private readonly IChatClient _chatClient;
|
private readonly IChatClient _chatClient;
|
||||||
private readonly AppDbContext _dbContext;
|
private readonly AppDbContext _dbContext;
|
||||||
|
private readonly ResiliencePipeline _retryPipeline;
|
||||||
|
private readonly AiSettings _settings;
|
||||||
private const string PromptVersion = "1.0";
|
private const string PromptVersion = "1.0";
|
||||||
private const string ModelId = "gemini-1.5-flash";
|
|
||||||
|
|
||||||
private static readonly ResiliencePipeline _retryPipeline = new ResiliencePipelineBuilder()
|
public KnowledgeService(
|
||||||
.AddRetry(new RetryStrategyOptions
|
IChatClient chatClient,
|
||||||
{
|
AppDbContext dbContext,
|
||||||
ShouldHandle = new PredicateBuilder().Handle<Exception>(ex =>
|
ResiliencePipelineProvider<string> pipelineProvider,
|
||||||
ex.Message.Contains("429") || ex.Message.Contains("Too Many Requests") || ex.Message.Contains("quota")),
|
IOptions<AiSettings> settings)
|
||||||
BackoffType = DelayBackoffType.Exponential,
|
|
||||||
UseJitter = true,
|
|
||||||
MaxRetryAttempts = 3,
|
|
||||||
Delay = TimeSpan.FromSeconds(2)
|
|
||||||
})
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
public KnowledgeService(IChatClient chatClient, AppDbContext dbContext)
|
|
||||||
{
|
{
|
||||||
_chatClient = chatClient;
|
_chatClient = chatClient;
|
||||||
_dbContext = dbContext;
|
_dbContext = dbContext;
|
||||||
|
_retryPipeline = pipelineProvider.GetPipeline("ai-retry");
|
||||||
|
_settings = settings.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<KnowledgePacket>> GetKnowledgeAsync(string text, CancellationToken cancellationToken = default)
|
public async Task<Result<KnowledgePacket>> GetKnowledgeAsync(string text, CancellationToken cancellationToken = default)
|
||||||
@@ -48,10 +45,9 @@ public class KnowledgeService : IKnowledgeService
|
|||||||
var normalizedText = ContentHasher.Normalize(text);
|
var normalizedText = ContentHasher.Normalize(text);
|
||||||
|
|
||||||
// Phase 4: Request Pre-processing (Token Saving)
|
// Phase 4: Request Pre-processing (Token Saving)
|
||||||
const int MaxInputLength = 15000; // Roughly 3k-4k tokens
|
if (normalizedText.Length > _settings.MaxInputLength)
|
||||||
if (normalizedText.Length > MaxInputLength)
|
|
||||||
{
|
{
|
||||||
return Result.Fail($"Input text is too long ({normalizedText.Length} characters after normalization). Max allowed is {MaxInputLength}.");
|
return Result.Fail($"Input text is too long ({normalizedText.Length} characters after normalization). Max allowed is {_settings.MaxInputLength}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple token estimation (4 chars per token)
|
// Simple token estimation (4 chars per token)
|
||||||
@@ -86,8 +82,8 @@ public class KnowledgeService : IKnowledgeService
|
|||||||
var options = new ChatOptions
|
var options = new ChatOptions
|
||||||
{
|
{
|
||||||
ResponseFormat = ChatResponseFormat.Json,
|
ResponseFormat = ChatResponseFormat.Json,
|
||||||
Temperature = 0.1f,
|
Temperature = (float)_settings.Temperature,
|
||||||
MaxOutputTokens = 1000
|
MaxOutputTokens = _settings.MaxOutputTokens
|
||||||
};
|
};
|
||||||
|
|
||||||
var response = await _retryPipeline.ExecuteAsync(async ct =>
|
var response = await _retryPipeline.ExecuteAsync(async ct =>
|
||||||
@@ -117,7 +113,7 @@ public class KnowledgeService : IKnowledgeService
|
|||||||
{
|
{
|
||||||
ContentHash = hash,
|
ContentHash = hash,
|
||||||
JsonData = jsonResponse,
|
JsonData = jsonResponse,
|
||||||
ModelId = ModelId,
|
ModelId = _settings.Model,
|
||||||
PromptVersion = PromptVersion,
|
PromptVersion = PromptVersion,
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ public static class PromptRegistry
|
|||||||
{
|
{
|
||||||
public const string KnowledgeExtractionSystemPrompt =
|
public const string KnowledgeExtractionSystemPrompt =
|
||||||
"You are an expert educator. Analyze the provided text to extract key concepts and generate relevant quizzes. " +
|
"You are an expert educator. Analyze the provided text to extract key concepts and generate relevant quizzes. " +
|
||||||
"Return ONLY a minified JSON object that strictly adheres to the provided schema. No markdown formatting, no explanations. " +
|
"CRITICAL: Return ONLY a minified JSON object. Do NOT include markdown formatting like ```json or ```. Do NOT include explanations. " +
|
||||||
"Schema: { \"concepts\": [ { \"title\": \"string\", \"description\": \"string\" } ], \"quizzes\": [ { \"question\": \"string\", \"options\": [ \"string\" ], \"correct_index\": 0 } ] }.";
|
"Schema: { \"concepts\": [ { \"title\": \"string\", \"description\": \"string\" } ], \"quizzes\": [ { \"question\": \"string\", \"options\": [ \"string\" ], \"correct_index\": 0 } ] }.";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user