refactor: share the AI draft pipeline between endpoint and upcoming automation
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bd6e272f57
commit
034b2ef790
2 files changed
+72
-41
No files matched your search
@@ -16,8 +16,6 @@ namespace FamilyPlanner.Api.Features.Learning;
|
||||
/// </summary>
|
||||
internal static class LearningAiDraftEndpoints
|
||||
{
|
||||
private const int MaxReasonsInMessage = 3;
|
||||
|
||||
public static void Map(RouteGroupBuilder group) =>
|
||||
group.MapPost("/sets/ai-draft", CreateDraftAsync)
|
||||
.Validate<CreateAiDraftRequest>()
|
||||
@@ -39,54 +37,22 @@ internal static class LearningAiDraftEndpoints
|
||||
return invalid;
|
||||
}
|
||||
|
||||
AiResult result;
|
||||
DraftGeneration generation;
|
||||
try
|
||||
{
|
||||
result = await ai.RunAsync(user.UserId() ?? Guid.Empty, AiPurpose.LearningGeneration, LearningAiPrompt.Build(request), ct);
|
||||
generation = await LearningAiDraftPipeline.GenerateAsync(ai, user.UserId() ?? Guid.Empty, request, ct);
|
||||
}
|
||||
catch (AiException ex)
|
||||
{
|
||||
return TypedResults.Problem(statusCode: ex.HttpStatus, title: "KI-Entwurf nicht möglich", detail: ex.Message);
|
||||
}
|
||||
|
||||
var parsed = LearningAiParser.Parse(result.Response.Json, request);
|
||||
if (parsed.Items.Count == 0)
|
||||
if (generation.Set is not { } set)
|
||||
{
|
||||
var detail = "Die KI hat keine brauchbaren Aufgaben geliefert. Bitte versuche es noch einmal oder formuliere den Wunsch anders.";
|
||||
if (parsed.DiscardReasons.Count > 0)
|
||||
{
|
||||
detail += " " + string.Join(' ', parsed.DiscardReasons.Take(MaxReasonsInMessage));
|
||||
}
|
||||
|
||||
return TypedResults.Problem(statusCode: StatusCodes.Status502BadGateway, title: "KI-Entwurf nicht möglich", detail: detail);
|
||||
return TypedResults.Problem(statusCode: StatusCodes.Status502BadGateway, title: "KI-Entwurf nicht möglich", detail: generation.FailureDetail);
|
||||
}
|
||||
|
||||
var exam = request.Mode == LearningMode.Exam;
|
||||
var fields = new CreateLearningSetRequest(
|
||||
parsed.Title,
|
||||
request.Subject,
|
||||
request.GradeLevel,
|
||||
request.TargetUserId,
|
||||
Mode: request.Mode,
|
||||
MaxAttempts: exam ? 1 : 0, // wie die Voreinstellung im Editor: Prüfung einmalig und ohne Lösungen, Übung unbegrenzt
|
||||
ShowSolutionsAfterSubmit: !exam,
|
||||
TotalPoints: 10);
|
||||
|
||||
var set = new LearningSet
|
||||
{
|
||||
Source = LearningSource.Ai,
|
||||
AiPrompt = LearningAiPrompt.StoredPrompt(request),
|
||||
AiProvider = Truncate(result.ProviderName, LearningSet.AiProviderMaxLength),
|
||||
AiModel = Truncate(result.Model, LearningSet.AiProviderMaxLength),
|
||||
};
|
||||
LearningManageEndpoints.ApplySet(set, fields);
|
||||
var order = 0;
|
||||
foreach (var input in parsed.Items)
|
||||
{
|
||||
var item = LearningValidation.ToItem(input);
|
||||
item.SortOrder = order++;
|
||||
set.Items.Add(item);
|
||||
}
|
||||
var parsed = generation.Parsed;
|
||||
|
||||
db.LearningSets.Add(set);
|
||||
await db.SaveChangesAsync(ct);
|
||||
@@ -96,6 +62,4 @@ internal static class LearningAiDraftEndpoints
|
||||
var dto = new LearningAiDraftDto(detailDto, request.Count, parsed.Discarded, parsed.Verified, parsed.NotVerified, parsed.Notes);
|
||||
return TypedResults.Created($"/api/learning/sets/{set.Id}", dto);
|
||||
}
|
||||
|
||||
private static string Truncate(string text, int max) => text.Length <= max ? text : text[..max];
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using FamilyPlanner.Api.Features.Ai;
|
||||
using FamilyPlanner.Domain.Ai;
|
||||
using FamilyPlanner.Domain.Learning;
|
||||
using FamilyPlanner.Shared.Learning;
|
||||
|
||||
namespace FamilyPlanner.Api.Features.Learning;
|
||||
|
||||
/// <summary>Ergebnis der KI-Erzeugung: das noch ungespeicherte Blatt (Entwurf) oder der Grund, warum keines entstand.</summary>
|
||||
internal sealed record DraftGeneration(LearningSet? Set, LearningAiParseResult Parsed, string FailureDetail);
|
||||
|
||||
/// <summary>
|
||||
/// Die gemeinsame Erzeugung von KI-Entwürfen (Phase 10b): Prompt bauen, <see cref="AiService"/> aufrufen, jede Aufgabe prüfen und ein ungespeichertes
|
||||
/// <see cref="LearningSet"/> mit <c>Source = Ai</c> aufbauen. Genutzt vom Endpunkt <c>ai-draft</c> und von der Automatik (Phase 11c, <see cref="LearningScheduleRunner"/>).
|
||||
/// Der Aufrufer speichert – bis dahin darf der Kontext nichts vormerken, weil <see cref="AiService"/> sein Nutzungsprotokoll über denselben Kontext schreibt.
|
||||
/// </summary>
|
||||
internal static class LearningAiDraftPipeline
|
||||
{
|
||||
private const int MaxReasonsInMessage = 3;
|
||||
|
||||
/// <exception cref="AiException">Konfiguration, Limit, Erreichbarkeit oder unbrauchbare Antwort der KI.</exception>
|
||||
public static async Task<DraftGeneration> GenerateAsync(AiService ai, Guid userId, CreateAiDraftRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await ai.RunAsync(userId, AiPurpose.LearningGeneration, LearningAiPrompt.Build(request), ct);
|
||||
var parsed = LearningAiParser.Parse(result.Response.Json, request);
|
||||
if (parsed.Items.Count == 0)
|
||||
{
|
||||
var detail = "Die KI hat keine brauchbaren Aufgaben geliefert. Bitte versuche es noch einmal oder formuliere den Wunsch anders.";
|
||||
if (parsed.DiscardReasons.Count > 0)
|
||||
{
|
||||
detail += " " + string.Join(' ', parsed.DiscardReasons.Take(MaxReasonsInMessage));
|
||||
}
|
||||
|
||||
return new DraftGeneration(null, parsed, detail);
|
||||
}
|
||||
|
||||
var exam = request.Mode == LearningMode.Exam;
|
||||
var fields = new CreateLearningSetRequest(
|
||||
parsed.Title,
|
||||
request.Subject,
|
||||
request.GradeLevel,
|
||||
request.TargetUserId,
|
||||
Mode: request.Mode,
|
||||
MaxAttempts: exam ? 1 : 0, // wie die Voreinstellung im Editor: Prüfung einmalig und ohne Lösungen, Übung unbegrenzt
|
||||
ShowSolutionsAfterSubmit: !exam,
|
||||
TotalPoints: 10);
|
||||
|
||||
var set = new LearningSet
|
||||
{
|
||||
Source = LearningSource.Ai,
|
||||
AiPrompt = LearningAiPrompt.StoredPrompt(request),
|
||||
AiProvider = Truncate(result.ProviderName, LearningSet.AiProviderMaxLength),
|
||||
AiModel = Truncate(result.Model, LearningSet.AiProviderMaxLength),
|
||||
};
|
||||
LearningManageEndpoints.ApplySet(set, fields);
|
||||
var order = 0;
|
||||
foreach (var input in parsed.Items)
|
||||
{
|
||||
var item = LearningValidation.ToItem(input);
|
||||
item.SortOrder = order++;
|
||||
set.Items.Add(item);
|
||||
}
|
||||
|
||||
return new DraftGeneration(set, parsed, string.Empty);
|
||||
}
|
||||
|
||||
private static string Truncate(string text, int max) => text.Length <= max ? text : text[..max];
|
||||
}
|
||||
Reference in new issue
Block a user