KI-Backend: Umfangs-Umschalter + Prompt Caching (4.5.15/4.5.16)

Umfangs-Umschalter: Checkbox "Auch bestehende Stundeninhalte anpassen" im
AiAssistDialog steuert, ob die KI bestehende Stunden inhaltlich ändern darf oder
die Einheit nur um neue Stunden erweitern soll. Zweifach durchgesetzt (Systemprompt
+ hartes client-seitiges Verwerfen in ApplyResponse), nicht nur der KI-Antwort
vertraut. Dabei auch einen Bug gefixt: eine bereits "Durchgeführt" markierte Stunde
wurde durch eine übernommene KI-Änderung stillschweigend auf "Geplant" zurückgesetzt.

Prompt Caching: der Systemprompt ist jetzt vollständig statisch (Voraussetzung für
Caching) und wird von AnthropicProvider.php als "cache_control: ephemeral" markiert
— wiederholte Anfragen innerhalb der 5-Minuten-TTL zahlen nur den reduzierten
Cache-Read-Preis. transactions-Tabelle und Preistabelle um Cache-Token-Spalten
erweitert, Migration für bereits deployte Installationen beigelegt.
This commit is contained in:
2026-08-16 16:06:52 +02:00
parent 4437f951d9
commit 1928916eac
14 changed files with 328 additions and 38 deletions
@@ -131,9 +131,15 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
};
}
public async Task<AiPlanningResponse> RequestPlanAsync(Unit unit, string instruction, string token)
public async Task<AiPlanningResponse> RequestPlanAsync(Unit unit, string instruction, string token,
bool allowModifyingExistingLessons = true)
{
var request = new AiPlanningRequest { Instruction = instruction, Unit = BuildContext(unit, instruction) };
var request = new AiPlanningRequest
{
Instruction = instruction,
Unit = BuildContext(unit, instruction),
AllowModifyingExistingLessons = allowModifyingExistingLessons,
};
using var req = new HttpRequestMessage(HttpMethod.Post, "plan.php")
{
@@ -172,16 +178,20 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
/// <see cref="ILessonRepository.Save"/> je Eintrag auf. Eine akzeptierte AiLesson mit einer Id,
/// die keiner tatsächlich zur Einheit gehörenden Lesson entspricht, wird NIE als Update
/// interpretiert, sondern immer als neue Lesson behandelt (Anti-Halluzinations-Absicherung).
/// Ist <paramref name="allowModifyingExistingLessons"/> false, werden Änderungen an bestehenden
/// Lessons zusätzlich hart verworfen (nicht nur per Systemprompt an die KI erbeten) — die
/// Einschränkung wird also nicht blind der KI-Antwort überlassen.
/// </summary>
public List<Lesson> ApplyResponse(Unit unit, List<AiLesson> acceptedLessons)
public List<Lesson> ApplyResponse(Unit unit, List<AiLesson> acceptedLessons, bool allowModifyingExistingLessons = true)
{
var existingIds = lessons.GetByUnit(unit.Id).Select(l => l.Id).ToHashSet();
var pathIdsByName = altPaths.GetAll().ToDictionary(p => p.Name, p => p.Id);
var existingLessons = lessons.GetByUnit(unit.Id).ToDictionary(l => l.Id);
var result = new List<Lesson>();
foreach (var ai in acceptedLessons)
{
var isUpdate = ai.Id is { } id && existingIds.Contains(id);
var isUpdate = ai.Id is { } id && existingLessons.ContainsKey(id);
if (isUpdate && !allowModifyingExistingLessons) continue;
result.Add(new Lesson
{
Id = isUpdate ? ai.Id!.Value : Guid.NewGuid(),
@@ -193,6 +203,10 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
StartTime = ai.StartTime,
Homework = ai.Homework,
Reflection = ai.Reflection,
// Status bleibt bei einer Änderung erhalten — sonst würde eine bereits
// durchgeführte Stunde durch eine KI-Anpassung stillschweigend auf "Geplant"
// zurückgesetzt (die KI kennt/liefert diesen Status gar nicht).
Status = isUpdate ? existingLessons[ai.Id!.Value].Status : LessonStatus.Planned,
Phases = ai.Phases.Select(p => new LessonPhaseStep
{
Name = p.Name,