CI / build-and-test (push) Canceled after 0s
- UntisHubService.RecordRun: ein abgeschlossener Langzeit-Fehlzeitenabgleich schliesst die kurzfristige Kadenz derselben Gruppe automatisch mit ab (nicht umgekehrt). - Fehlzeitenabgleich-Dialog: neue "Uebernahme als"-ComboBox statt starrem Zielstatus, vorbelegt mit dem berechneten Vorschlag, aber frei aenderbar. - Neuer ai-backend-Endpunkt untis-status.php + AiPlanningService.RequestUntisStatusSuggestionsAsync: gebuendelter, anonymisierter KI-Statusvorschlag (nur Positions-Id + Rohsignale, nie Name/Klasse/ Datum), mit hartem Id-Mengen-Abgleich gegen Verwechslung. - Neue MCP-Tools (UntisComparisonTools): get_untis_hub_status, get_untis_absence_rows/ apply_untis_absence_status (anonymer Weg ueber ENr-Zuordnung) sowie get_named_untis_absence_pattern als bewusste, eng begrenzte Ausnahme (Name+Fehlzeiten fuer explizit angegebene Schueler-IDs, mit Bestaetigung ohne Sitzungsfreigabe - dafuer IMcpConfirmationService.ConfirmAsync um allowSessionTrust erweitert). - MapStatus/ENr-Zuordnung aus dem ViewModel in das neue, geteilte UntisLessonAbsenceHelper gezogen, damit Dialog und MCP-Tool nie unterschiedliche Statusvorschlaege berechnen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
232 lines
10 KiB
C#
232 lines
10 KiB
C#
namespace LehrerApp.Core.AiPlanning;
|
|
|
|
/// <summary>
|
|
/// Wire-Vertrag mit dem externen KI-Backend (siehe ai-backend/, TODO 4.5.9). Bewusst getrennt von
|
|
/// den internen Domänenmodellen in Core/Models: dieser Vertrag muss unabhängig von internen
|
|
/// Domänen-Refactors abwärtskompatibel zum deployten PHP-Backend bleiben.
|
|
/// </summary>
|
|
public class AiPlanningRequest
|
|
{
|
|
public string Instruction { get; set; } = "";
|
|
public AiUnitContext Unit { get; set; } = new();
|
|
// Steuert, ob die KI bestehende Lessons inhaltlich ändern darf, oder nur neue Lessons
|
|
// vorschlagen soll (Einheit umplanen ohne vs. mit Ändern bestehender Stunden). Wird sowohl im
|
|
// Systemprompt des Backends durchgesetzt als auch client-seitig in ApplyResponse defensiv
|
|
// geprüft — die KI-Antwort wird dafür nicht blind vertraut.
|
|
public bool AllowModifyingExistingLessons { get; set; } = true;
|
|
// Gesetzt, wenn die Anfrage aus dem Editor einer einzelnen Stunde heraus gestartet wurde
|
|
// (statt aus der Einheiten-Übersicht): die KI soll dann AUSSCHLIESSLICH diese eine Stunde
|
|
// bearbeiten, keine neuen Stunden vorschlagen und keine andere bestehende Stunde anfassen.
|
|
// Wie AllowModifyingExistingLessons sowohl im Systemprompt erbeten als auch client-seitig
|
|
// hart durchgesetzt (siehe AiAssistDialogViewModel.Send/AiPlanningService.ApplyResponse).
|
|
public Guid? FocusLessonId { get; set; }
|
|
}
|
|
|
|
public class AiUnitContext
|
|
{
|
|
public Guid Id { get; set; }
|
|
public string Title { get; set; } = "";
|
|
public DateOnly? StartDate { get; set; }
|
|
public DateOnly? EndDate { get; set; }
|
|
public List<string> Competencies { get; set; } = [];
|
|
public string? Notes { get; set; }
|
|
|
|
// Nur Kontext für die KI, wird nicht zurückerwartet.
|
|
public string SubjectName { get; set; } = "";
|
|
public int GradeLevel { get; set; }
|
|
public string GroupName { get; set; } = "";
|
|
public List<AiCompetencyDomain> CompetencyCatalog { get; set; } = [];
|
|
public List<AiAlternativePath> AlternativePathCatalog { get; set; } = [];
|
|
|
|
public List<AiLesson> Lessons { get; set; } = [];
|
|
}
|
|
|
|
public class AiCompetencyDomain
|
|
{
|
|
public string Name { get; set; } = "";
|
|
public List<AiCompetencyItem> Items { get; set; } = [];
|
|
}
|
|
|
|
public class AiCompetencyItem
|
|
{
|
|
public string Code { get; set; } = "";
|
|
public string Description { get; set; } = "";
|
|
}
|
|
|
|
public class AiAlternativePath
|
|
{
|
|
public Guid Id { get; set; }
|
|
public string Name { get; set; } = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="Id"/> ist der einzige Unterscheidungsmechanismus neu/geändert: gesetzt und einer
|
|
/// tatsächlich im Request gesendeten Lesson zugehörig = Änderung; null oder eine unbekannte Id =
|
|
/// neue Lesson. Eine unbekannte Id wird beim Import NIE als Update einer bestehenden Lesson
|
|
/// interpretiert (siehe AiPlanningService.ApplyResponse) — sonst könnte eine halluzinierte Id im
|
|
/// schlimmsten Fall eine fremde Lesson überschreiben.
|
|
/// </summary>
|
|
public class AiLesson
|
|
{
|
|
public Guid? Id { get; set; }
|
|
public DateOnly? Date { get; set; }
|
|
public int? LessonNumber { get; set; }
|
|
public string Topic { get; set; } = "";
|
|
public TimeOnly? StartTime { get; set; }
|
|
public List<AiPhaseStep> Phases { get; set; } = [];
|
|
public string? Homework { get; set; }
|
|
public string? Reflection { get; set; }
|
|
}
|
|
|
|
public class AiPhaseStep
|
|
{
|
|
public string Name { get; set; } = "";
|
|
public int DurationMinutes { get; set; }
|
|
public string Activity { get; set; } = "";
|
|
public string Material { get; set; } = "";
|
|
public string Shorthand { get; set; } = "";
|
|
// Name statt Guid: LLMs erfinden/verändern Guids unzuverlässig, ein Name ist beim Import
|
|
// gegen den Katalog abgleichbar (kein Treffer -> null = Hauptweg, kein Fehler).
|
|
public string? AlternativePathName { get; set; }
|
|
// Kurzer Vorschlag für ein konkretes Medium/Material zu dieser Phase (z.B. Tafelbild,
|
|
// Arbeitsblatt), nur gesetzt, wenn eines wirklich sinnvoll wäre — nicht bei jeder Phase.
|
|
// Wird NICHT in Lesson.Phases übernommen (nur für die Review-Anzeige/Prompt-Erzeugung
|
|
// relevant, siehe AiPlanningService.BuildMaterialPrompt), daher kein Feld auf
|
|
// LessonPhaseStep nötig.
|
|
public string? MaterialSuggestion { get; set; }
|
|
}
|
|
|
|
public class AiPlanningResponse
|
|
{
|
|
public List<AiLesson> Lessons { get; set; } = [];
|
|
public string? Summary { get; set; }
|
|
// Vom Backend zusätzlich mitgelieferte, unveränderte Modellantwort. Wird nur benötigt, falls
|
|
// die typisierte Deserialisierung scheitert; bei einer regulär verarbeiteten Antwort wird sie
|
|
// weder angezeigt noch gespeichert.
|
|
public string? RawResponse { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wire-Vertrag für den "Schattenfeld"-Endpunkt (4.5.21, ai-backend/explain.php): fragt nur auf
|
|
/// Nachfrage einen didaktischen Hintergrund zu einer bereits vorgeschlagenen/geplanten Lesson ab
|
|
/// (Warum dieser Aufbau, mögliche Stolpersteine, Differenzierung) — ändert nichts an der Planung,
|
|
/// eigener Endpunkt statt Zusatzfeld in jeder plan.php-Antwort, damit nur bei tatsächlicher
|
|
/// Nutzung abgerechnet wird.
|
|
/// </summary>
|
|
public class AiExplainRequest
|
|
{
|
|
public AiUnitContext Unit { get; set; } = new();
|
|
public AiLesson Lesson { get; set; } = new();
|
|
}
|
|
|
|
public class AiExplainResponse
|
|
{
|
|
public string Explanation { get; set; } = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wire-Vertrag für den Gefährdungsbeurteilungs-Entwurf (ai-backend/gbu.php, Nutzerwunsch neben
|
|
/// 4.2 "Anhänge je Stunde"): erkennt aus Thema/Verlaufsplan der Lesson das Experiment und liefert
|
|
/// dafür einen strukturierten Entwurf. Eigener Endpunkt statt Zusatzfeld in plan.php/explain.php,
|
|
/// da inhaltlich unabhängig von der Unterrichtsplanung selbst.
|
|
/// </summary>
|
|
public class AiHazardAssessmentRequest
|
|
{
|
|
public AiUnitContext Unit { get; set; } = new();
|
|
public AiLesson Lesson { get; set; } = new();
|
|
}
|
|
|
|
public class AiHazardAssessmentResponse
|
|
{
|
|
/// "Lehrerversuch" | "Schuelerversuch" | "Demonstrationsversuch" (siehe ExperimentKind).
|
|
public string ExperimentKind { get; set; } = "";
|
|
public string Procedure { get; set; } = "";
|
|
public List<AiHazardSubstance> Substances { get; set; } = [];
|
|
public List<string> Hazards { get; set; } = [];
|
|
public List<string> ProtectiveMeasures { get; set; } = [];
|
|
public string FirstAid { get; set; } = "";
|
|
public string Disposal { get; set; } = "";
|
|
}
|
|
|
|
public class AiHazardSubstance
|
|
{
|
|
public string Name { get; set; } = "";
|
|
public string Amount { get; set; } = "";
|
|
/// GHS-Piktogramm-Codes als Text, z.B. "GHS05" — siehe GhsPictogram für die Zuordnung.
|
|
public List<string> GhsPictograms { get; set; } = [];
|
|
public string HStatements { get; set; } = "";
|
|
public string PStatements { get; set; } = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wire-Vertrag für die Chemikalien-Recherche (ai-backend/substance.php, Nutzerwunsch neben 4.2):
|
|
/// ein reiner Datenbank-Lookup gegen die lokale RiSU-Stoffliste, kein KI-Aufruf (siehe
|
|
/// ai-backend/stoffliste.php) — der Name "Ai..." bleibt trotzdem bestehen, da der Aufruf über
|
|
/// dieselbe Anmeldung/denselben Host wie die echten KI-Endpunkte läuft. Eigener, sehr schmaler
|
|
/// Wire-Vertrag statt Wiederverwendung von AiHazardSubstance — die Recherche liefert
|
|
/// zusätzliche, nur hier relevante Felder (CAS, Signalwort, Tätigkeitsbeschränkung, Quelle).
|
|
/// Enthält bewusst KEIN Entsorgungsfeld — die RiSU-Stoffliste führt keine Entsorgungshinweise.
|
|
/// </summary>
|
|
public class AiSubstanceResearchResponse
|
|
{
|
|
public string Name { get; set; } = "";
|
|
public string Cas { get; set; } = "";
|
|
public List<string> GhsPictograms { get; set; } = [];
|
|
public string SignalWord { get; set; } = "";
|
|
public string HStatements { get; set; } = "";
|
|
public string PStatements { get; set; } = "";
|
|
public string ActivityRestriction { get; set; } = "";
|
|
public string Source { get; set; } = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wire-Vertrag für den Fehlzeiten-Statusvorschlag (ai-backend/untis-status.php, Nutzer-Feedback
|
|
/// zum Untis-Hub: der aus WebUntis abgeleitete Zielstatus war über den Anzeigetext oft nicht
|
|
/// nachvollziehbar). Bewusst OHNE jeden Personenbezug - <see cref="AiUntisStatusRow.Id"/> ist eine
|
|
/// rein technische, für die KI bedeutungslose Kennung, über die der Desktop-Client die Antwort
|
|
/// zurückordnet; kein Name, keine Klasse, kein Datum verlässt damit die App. Mehrere Zeilen eines
|
|
/// Abgleichslaufs werden in einer Anfrage gebündelt statt je Zeile einzeln (Kosten/Latenz).
|
|
/// </summary>
|
|
public class AiUntisStatusRequest
|
|
{
|
|
public List<AiUntisStatusRow> Rows { get; set; } = [];
|
|
}
|
|
|
|
public class AiUntisStatusRow
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string ReasonText { get; set; } = "";
|
|
public int AbsentMinutes { get; set; }
|
|
/// True, wenn WebUntis für den Eintrag bereits ein Bearbeitungsdatum führt (unabhängig vom
|
|
/// tatsächlichen Datum, das aus Datenschutzgründen nicht mitgeschickt wird).
|
|
public bool HandledOn { get; set; }
|
|
/// Schulinterne Konvention: Klammerung der Entschuldigungsnummer bedeutet unentschuldigt, ohne
|
|
/// Klammern entschuldigt; null, wenn keine Nummer hinterlegt ist (siehe UntisDiffService-Analog
|
|
/// in WebUntisLessonAbsenceComparisonViewModel.MapStatus).
|
|
public bool? ExternKeyInParentheses { get; set; }
|
|
/// Bereits regelbasiert ermittelter Status (siehe MapStatus) - der Systemprompt bittet die KI,
|
|
/// nur bei eindeutigem Widerspruch im Freitext davon abzuweichen, statt bei Unsicherheit zu raten.
|
|
public string CurrentGuess { get; set; } = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enthält absichtlich GENAU eine Zeile je gesendeter <see cref="AiUntisStatusRow.Id"/> - der
|
|
/// Client verwirft die gesamte Antwort, wenn die zurückgegebene Id-Menge nicht exakt der
|
|
/// gesendeten entspricht (siehe AiPlanningService.RequestUntisStatusSuggestionsAsync), statt sich
|
|
/// auf die Reihenfolge zu verlassen. So bleibt eine Verwechslung zwischen Vorschlag und Zeile
|
|
/// strukturell ausgeschlossen, nicht nur im Regelfall vermieden.
|
|
/// </summary>
|
|
public class AiUntisStatusResponse
|
|
{
|
|
public List<AiUntisStatusSuggestion> Suggestions { get; set; } = [];
|
|
}
|
|
|
|
public class AiUntisStatusSuggestion
|
|
{
|
|
public string Id { get; set; } = "";
|
|
/// Einer von "Present"/"Late"/"LeftDuringClass"/"ExcusePending"/"Excused"/"Unexcused" (siehe
|
|
/// Systemprompt in ai-backend/untis-status.php) - wird client-seitig gegen genau diese Menge
|
|
/// geprüft, bevor er als AttendanceStatus interpretiert wird.
|
|
public string Status { get; set; } = "";
|
|
}
|