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:
@@ -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,
|
||||
|
||||
@@ -1113,6 +1113,7 @@ public partial class AiAssistDialogViewModel : ObservableObject
|
||||
private readonly Unit _unit;
|
||||
|
||||
[ObservableProperty] private string _instruction = "";
|
||||
[ObservableProperty] private bool _allowModifyingExisting = true;
|
||||
[ObservableProperty] private bool _isBusy;
|
||||
[ObservableProperty] private string _errorMessage = "";
|
||||
[ObservableProperty] private bool _hasResults;
|
||||
@@ -1143,12 +1144,19 @@ public partial class AiAssistDialogViewModel : ObservableObject
|
||||
ErrorMessage = ""; IsBusy = true;
|
||||
try
|
||||
{
|
||||
var response = await _aiPlanning.RequestPlanAsync(_unit, Instruction, token);
|
||||
var response = await _aiPlanning.RequestPlanAsync(_unit, Instruction, token, AllowModifyingExisting);
|
||||
var existingIds = _lessons.GetByUnit(_unit.Id).Select(l => l.Id).ToHashSet();
|
||||
|
||||
ReviewItems.Clear();
|
||||
foreach (var l in response.Lessons)
|
||||
ReviewItems.Add(new AiLessonReviewItem(l, isNew: l.Id is not { } id || !existingIds.Contains(id)));
|
||||
{
|
||||
var isExisting = l.Id is { } id && existingIds.Contains(id);
|
||||
// Falls der Modus Änderungen an bestehenden Stunden verbietet, aber die KI die
|
||||
// Anweisung trotzdem ignoriert hat: gar nicht erst zur Übernahme anbieten, statt
|
||||
// dem Nutzer eine Auswahl zu zeigen, die ApplyResponse ohnehin verwerfen würde.
|
||||
if (isExisting && !AllowModifyingExisting) continue;
|
||||
ReviewItems.Add(new AiLessonReviewItem(l, isNew: !isExisting));
|
||||
}
|
||||
Summary = response.Summary;
|
||||
HasResults = true;
|
||||
}
|
||||
@@ -1160,7 +1168,7 @@ public partial class AiAssistDialogViewModel : ObservableObject
|
||||
private void Apply()
|
||||
{
|
||||
var accepted = ReviewItems.Where(i => i.Accepted).Select(i => i.Source).ToList();
|
||||
foreach (var lesson in _aiPlanning.ApplyResponse(_unit, accepted))
|
||||
foreach (var lesson in _aiPlanning.ApplyResponse(_unit, accepted, AllowModifyingExisting))
|
||||
_lessons.Save(lesson);
|
||||
Result = true;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,14 @@
|
||||
<TextBlock Text="KI-Unterstützung" Classes="dialogtitle"/>
|
||||
<TextBlock Text="{Binding UnitSummary}" FontSize="12" Opacity="0.6"/>
|
||||
|
||||
<StackPanel Spacing="4" IsVisible="{Binding !HasResults}">
|
||||
<StackPanel Spacing="8" IsVisible="{Binding !HasResults}">
|
||||
<TextBlock Text="Anweisung" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Instruction}" AcceptsReturn="True" TextWrapping="Wrap" Height="120"
|
||||
PlaceholderText="z.B. Ergänze zwei weitere Stunden zum Thema Redoxreaktionen mit steigendem Anspruch."
|
||||
IsEnabled="{Binding !IsBusy}"/>
|
||||
<CheckBox Content="Auch bestehende Stundeninhalte anpassen" IsChecked="{Binding AllowModifyingExisting}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
ToolTip.Tip="Deaktivieren, um die Einheit nur um neue Stunden zu erweitern, ohne den Inhalt bereits vorhandener Stunden zu verändern."/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Anfrage läuft…" FontSize="12" Opacity="0.6" IsVisible="{Binding IsBusy}"/>
|
||||
|
||||
Reference in New Issue
Block a user