feat: implement AI-driven knowledge extraction service with semantic caching and persistent storage
This commit is contained in:
@@ -0,0 +1,87 @@
|
|||||||
|
# 🤖 LLM Agent Implementation Backlog: AI Semantic Integration
|
||||||
|
|
||||||
|
**Project Context:** .NET 10, EF Core (SQLite), `Microsoft.Extensions.AI`.
|
||||||
|
**Core Goal:** Integrate Gemini 1.5 Flash with a persistent Semantic Cache to minimize API costs and latency.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Phase 1: Persistence & Domain Layer
|
||||||
|
**Objective:** Define the storage schema to prevent redundant AI calls.
|
||||||
|
|
||||||
|
### Task 1.1: Create `SemanticKnowledgeCache` Entity
|
||||||
|
* **Target Folder:** `Core/Entities` or `Infrastructure/Persistence/Entities`.
|
||||||
|
* **Requirements:**
|
||||||
|
* Create a class `SemanticKnowledgeCache`.
|
||||||
|
* **Properties:**
|
||||||
|
* `string ContentHash` (Key, Fixed length 64).
|
||||||
|
* `string JsonData` (Required, stores the serialized AI output).
|
||||||
|
* `string ModelId` (Default: "gemini-1.5-flash").
|
||||||
|
* `string PromptVersion` (Default: "1.0").
|
||||||
|
* `DateTime CreatedAt` (UTC).
|
||||||
|
* **LLM Instructions:** "Generate an EF Core entity for SemanticKnowledgeCache. Ensure `ContentHash` has a Unique Index for O(1) lookups."
|
||||||
|
|
||||||
|
### Task 1.2: Implement Hashing Utility
|
||||||
|
* **Target Folder:** `Core/Helpers` or `Infrastructure/Security`.
|
||||||
|
* **Requirements:**
|
||||||
|
* Create `ContentHasher` class.
|
||||||
|
* Method `string ComputeHash(string input)`.
|
||||||
|
* **Logic:** Normalize input (Trim, lower-case) -> Compute SHA-256 -> Return Hex string.
|
||||||
|
* **LLM Instructions:** "Create a thread-safe utility to generate SHA-256 hashes from strings. Ensure it handles nulls and whitespace consistently."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 Phase 2: AI Client & Contract Definition
|
||||||
|
**Objective:** Set up the communication bridge with Google Gemini API.
|
||||||
|
|
||||||
|
### Task 2.1: Define Data Transfer Objects (DTOs)
|
||||||
|
* **Target Folder:** `Core/DTOs/AI`.
|
||||||
|
* **Requirements:**
|
||||||
|
* Define `KnowledgePacket` record containing `List<KeyConcept>` and `List<QuizQuestion>`.
|
||||||
|
* 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."
|
||||||
|
|
||||||
|
### Task 2.2: Infrastructure AI Client Setup
|
||||||
|
* **Target:** `Program.cs` / Dependency Injection.
|
||||||
|
* **Requirements:**
|
||||||
|
* Install `Microsoft.Extensions.AI` and `Microsoft.Extensions.AI.Google`.
|
||||||
|
* Register `IChatClient` using `GoogleChatClient`.
|
||||||
|
* Inject `ApiKey` from `IConfiguration`.
|
||||||
|
* **LLM Instructions:** "Register the GoogleChatClient in the DI container. Use the .NET 10 `AddChatClient` extension pattern."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Phase 3: Service Orchestration (The "Smart" Logic)
|
||||||
|
**Objective:** Implement the caching proxy logic.
|
||||||
|
|
||||||
|
### Task 3.1: Create `KnowledgeService` Implementation
|
||||||
|
* **Target Folder:** `Application/Services`.
|
||||||
|
* **Logic Flow:**
|
||||||
|
1. `hash = ContentHasher.ComputeHash(inputText)`.
|
||||||
|
2. `cached = await dbContext.Cache.FirstOrDefaultAsync(h => h.ContentHash == hash)`.
|
||||||
|
3. If `cached` exists AND `PromptVersion` matches -> Deserialize and return.
|
||||||
|
4. Else -> Call `IChatClient.CompleteAsync<KnowledgePacket>(...)`.
|
||||||
|
5. Save result to DB with the hash -> Return.
|
||||||
|
* **LLM Instructions:** "Implement a service that acts as a proxy between the UI and the Gemini API. It must prioritize SQLite cache hits over API calls."
|
||||||
|
|
||||||
|
### Task 3.2: System Prompt Engineering
|
||||||
|
* **Requirements:**
|
||||||
|
* Create a `PromptRegistry` class.
|
||||||
|
* **System Message:** "You are an educational assistant. Analyze the text and output ONLY valid minified JSON. Schema: { 'concepts': [], 'quizzes': [] }. Do not include markdown formatting like \` \` \` json."
|
||||||
|
* **LLM Instructions:** "Craft a high-precision system prompt for Gemini 1.5 Flash to ensure it returns parseable JSON without unnecessary tokens."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ Phase 4: Resilience & Optimization
|
||||||
|
**Objective:** Handle API limits and monitor performance.
|
||||||
|
|
||||||
|
### Task 4.1: Resilience Pipeline (Polly)
|
||||||
|
* **Requirements:**
|
||||||
|
* Implement an `HttpRetry` policy specifically for `429 Too Many Requests`.
|
||||||
|
* Use Exponential Backoff with Jitter.
|
||||||
|
* **LLM Instructions:** "Add a resilience pipeline to the AI client using Polly. Handle rate-limiting gracefully to stay within the Gemini Free Tier limits."
|
||||||
|
|
||||||
|
### Task 4.2: Request Pre-processing (Token Saving)
|
||||||
|
* **Logic:**
|
||||||
|
* Check input string length.
|
||||||
|
* If `length > threshold`, truncate or throw an error to prevent massive token spend.
|
||||||
|
* **LLM Instructions:** "Add a guard clause to the KnowledgeService to validate input size before calling the API. Log the estimated token count."
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main()
|
||||||
|
{
|
||||||
|
string input1 = "Hello \n World";
|
||||||
|
string input2 = "Hello World";
|
||||||
|
|
||||||
|
string norm1 = Normalize(input1);
|
||||||
|
string norm2 = Normalize(input2);
|
||||||
|
|
||||||
|
Console.WriteLine($"Input 1: '{input1}' -> Normalized: '{norm1}'");
|
||||||
|
Console.WriteLine($"Input 2: '{input2}' -> Normalized: '{norm2}'");
|
||||||
|
Console.WriteLine($"Match: {norm1 == norm2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Normalize(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input)) return string.Empty;
|
||||||
|
var normalized = Regex.Replace(input.Trim(), @"\s+", " ");
|
||||||
|
return normalized.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using FluentResults;
|
||||||
|
using NexusReader.Application.DTOs.AI;
|
||||||
|
|
||||||
|
namespace NexusReader.Application.Abstractions.Services;
|
||||||
|
|
||||||
|
public interface IKnowledgeService
|
||||||
|
{
|
||||||
|
Task<Result<KnowledgePacket>> GetKnowledgeAsync(string text, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NexusReader.Application.DTOs.AI;
|
||||||
|
|
||||||
|
public record KeyConcept(
|
||||||
|
[property: JsonPropertyName("title")] string Title,
|
||||||
|
[property: JsonPropertyName("description")] string Description
|
||||||
|
);
|
||||||
|
|
||||||
|
public record QuizQuestion(
|
||||||
|
[property: JsonPropertyName("question")] string Question,
|
||||||
|
[property: JsonPropertyName("options")] List<string> Options,
|
||||||
|
[property: JsonPropertyName("correct_index")] int CorrectIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
public record KnowledgePacket(
|
||||||
|
[property: JsonPropertyName("concepts")] List<KeyConcept> Concepts,
|
||||||
|
[property: JsonPropertyName("quizzes")] List<QuizQuestion> Quizzes
|
||||||
|
);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace NexusReader.Domain.Entities;
|
||||||
|
|
||||||
|
public class SemanticKnowledgeCache
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
[MaxLength(64)]
|
||||||
|
public string ContentHash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string JsonData { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string ModelId { get; set; } = "gemini-1.5-flash";
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(10)]
|
||||||
|
public string PromptVersion { get; set; } = "1.0";
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
using GeminiDotnet;
|
||||||
|
using GeminiDotnet.Extensions.AI;
|
||||||
|
using NexusReader.Infrastructure.Persistence;
|
||||||
using NexusReader.Application.Abstractions.Services;
|
using NexusReader.Application.Abstractions.Services;
|
||||||
using NexusReader.Infrastructure.Services;
|
using NexusReader.Infrastructure.Services;
|
||||||
|
|
||||||
@@ -6,8 +12,26 @@ namespace NexusReader.Infrastructure;
|
|||||||
|
|
||||||
public static class DependencyInjection
|
public static class DependencyInjection
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
|
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
|
var connectionString = configuration.GetConnectionString("SqliteConnection") ?? "Data Source=nexus.db";
|
||||||
|
services.AddDbContext<AppDbContext>(options =>
|
||||||
|
options.UseSqlite(connectionString));
|
||||||
|
|
||||||
|
var apiKey = configuration["Ai:Google:ApiKey"];
|
||||||
|
if (string.IsNullOrWhiteSpace(apiKey))
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
ApiKey = apiKey,
|
||||||
|
ModelId = modelId
|
||||||
|
}));
|
||||||
|
|
||||||
|
services.AddScoped<IKnowledgeService, KnowledgeService>();
|
||||||
services.AddTransient<IAiGenerateQuizService, FakeAiGenerateQuizService>();
|
services.AddTransient<IAiGenerateQuizService, FakeAiGenerateQuizService>();
|
||||||
services.AddTransient<IEpubService, EpubService>();
|
services.AddTransient<IEpubService, EpubService>();
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace NexusReader.Infrastructure.Helpers;
|
||||||
|
|
||||||
|
public static class ContentHasher
|
||||||
|
{
|
||||||
|
public static string ComputeHash(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedInput = Normalize(input);
|
||||||
|
|
||||||
|
var inputBytes = Encoding.UTF8.GetBytes(normalizedInput);
|
||||||
|
var hashBytes = SHA256.HashData(inputBytes);
|
||||||
|
|
||||||
|
return Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Normalize(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim and collapse all consecutive whitespace characters into a single space
|
||||||
|
var normalized = Regex.Replace(input.Trim(), @"\s+", " ");
|
||||||
|
|
||||||
|
// Convert to lower-case as AI analysis is generally not case-sensitive for concepts/quizzes
|
||||||
|
return normalized.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,15 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="GeminiDotnet.Extensions.AI" Version="0.23.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.AI" Version="10.5.0" />
|
||||||
|
<PackageReference Include="Polly" Version="8.6.6" />
|
||||||
|
<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" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NexusReader.Domain.Entities;
|
||||||
|
|
||||||
|
namespace NexusReader.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
public class AppDbContext : DbContext
|
||||||
|
{
|
||||||
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<SemanticKnowledgeCache> SemanticKnowledgeCache => Set<SemanticKnowledgeCache>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SemanticKnowledgeCache>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.ContentHash);
|
||||||
|
entity.HasIndex(e => e.ContentHash).IsUnique();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using FluentResults;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
using NexusReader.Application.Abstractions.Services;
|
||||||
|
using NexusReader.Application.DTOs.AI;
|
||||||
|
using NexusReader.Domain.Entities;
|
||||||
|
using NexusReader.Infrastructure.Helpers;
|
||||||
|
using NexusReader.Infrastructure.Persistence;
|
||||||
|
using Polly;
|
||||||
|
using Polly.Retry;
|
||||||
|
|
||||||
|
namespace NexusReader.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class KnowledgeService : IKnowledgeService
|
||||||
|
{
|
||||||
|
private readonly IChatClient _chatClient;
|
||||||
|
private readonly AppDbContext _dbContext;
|
||||||
|
private const string PromptVersion = "1.0";
|
||||||
|
private const string ModelId = "gemini-1.5-flash";
|
||||||
|
|
||||||
|
private static readonly ResiliencePipeline _retryPipeline = new ResiliencePipelineBuilder()
|
||||||
|
.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 = 3,
|
||||||
|
Delay = TimeSpan.FromSeconds(2)
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
public KnowledgeService(IChatClient chatClient, AppDbContext dbContext)
|
||||||
|
{
|
||||||
|
_chatClient = chatClient;
|
||||||
|
_dbContext = dbContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<KnowledgePacket>> GetKnowledgeAsync(string text, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return Result.Fail("Input text is empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize text to ensure consistent hashing and reduce token noise
|
||||||
|
var normalizedText = ContentHasher.Normalize(text);
|
||||||
|
|
||||||
|
// Phase 4: Request Pre-processing (Token Saving)
|
||||||
|
const int MaxInputLength = 15000; // Roughly 3k-4k tokens
|
||||||
|
if (normalizedText.Length > MaxInputLength)
|
||||||
|
{
|
||||||
|
return Result.Fail($"Input text is too long ({normalizedText.Length} characters after normalization). Max allowed is {MaxInputLength}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple token estimation (4 chars per token)
|
||||||
|
var estimatedTokens = normalizedText.Length / 4;
|
||||||
|
Console.WriteLine($"[KnowledgeService] Processing request with ~{estimatedTokens} tokens.");
|
||||||
|
|
||||||
|
var hash = ContentHasher.ComputeHash(normalizedText);
|
||||||
|
|
||||||
|
// 1. Check Cache
|
||||||
|
var cached = await _dbContext.SemanticKnowledgeCache
|
||||||
|
.FirstOrDefaultAsync(c => c.ContentHash == hash && c.PromptVersion == PromptVersion, cancellationToken);
|
||||||
|
|
||||||
|
if (cached != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var packet = JsonSerializer.Deserialize<KnowledgePacket>(cached.JsonData);
|
||||||
|
if (packet != null)
|
||||||
|
{
|
||||||
|
return Result.Ok(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
// If deserialization fails, we proceed to call the AI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Call AI Client
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new ChatOptions
|
||||||
|
{
|
||||||
|
ResponseFormat = ChatResponseFormat.Json,
|
||||||
|
Temperature = 0.1f,
|
||||||
|
MaxOutputTokens = 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _retryPipeline.ExecuteAsync(async ct =>
|
||||||
|
await _chatClient.GetResponseAsync(new List<ChatMessage>
|
||||||
|
{
|
||||||
|
new ChatMessage(ChatRole.System, PromptRegistry.KnowledgeExtractionSystemPrompt),
|
||||||
|
new ChatMessage(ChatRole.User, normalizedText)
|
||||||
|
}, options, cancellationToken: ct), cancellationToken);
|
||||||
|
|
||||||
|
var jsonResponse = response.Text;
|
||||||
|
if (string.IsNullOrWhiteSpace(jsonResponse))
|
||||||
|
{
|
||||||
|
return Result.Fail("AI returned an empty response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup potential markdown if Gemini still adds it despite options
|
||||||
|
jsonResponse = jsonResponse.Replace("```json", "").Replace("```", "").Trim();
|
||||||
|
|
||||||
|
var knowledgePacket = JsonSerializer.Deserialize<KnowledgePacket>(jsonResponse);
|
||||||
|
if (knowledgePacket == null)
|
||||||
|
{
|
||||||
|
return Result.Fail("Failed to deserialize AI response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Save to Cache
|
||||||
|
var cacheEntry = new SemanticKnowledgeCache
|
||||||
|
{
|
||||||
|
ContentHash = hash,
|
||||||
|
JsonData = jsonResponse,
|
||||||
|
ModelId = ModelId,
|
||||||
|
PromptVersion = PromptVersion,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle potential race condition if multiple requests for same text arrive
|
||||||
|
if (cached == null)
|
||||||
|
{
|
||||||
|
_dbContext.SemanticKnowledgeCache.Add(cacheEntry);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cached.JsonData = jsonResponse;
|
||||||
|
cached.CreatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(knowledgePacket);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error("Failed to extract knowledge from AI").CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace NexusReader.Infrastructure.Services;
|
||||||
|
|
||||||
|
public static class PromptRegistry
|
||||||
|
{
|
||||||
|
public const string KnowledgeExtractionSystemPrompt =
|
||||||
|
"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. " +
|
||||||
|
"Schema: { \"concepts\": [ { \"title\": \"string\", \"description\": \"string\" } ], \"quizzes\": [ { \"question\": \"string\", \"options\": [ \"string\" ], \"correct_index\": 0 } ] }.";
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ builder.Services.AddScoped<IFocusModeService, FocusModeService>();
|
|||||||
builder.Services.AddScoped<IReaderNavigationService, ReaderNavigationService>();
|
builder.Services.AddScoped<IReaderNavigationService, ReaderNavigationService>();
|
||||||
|
|
||||||
builder.Services.AddApplication();
|
builder.Services.AddApplication();
|
||||||
builder.Services.AddInfrastructure();
|
builder.Services.AddInfrastructure(builder.Configuration);
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|||||||
@@ -5,5 +5,14 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"SqliteConnection": "Data Source=nexus.db"
|
||||||
|
},
|
||||||
|
"Ai": {
|
||||||
|
"Google": {
|
||||||
|
"ApiKey": "PLACEHOLDER",
|
||||||
|
"Model": "gemini-1.5-flash"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user