Freigabe durch die Leitung innerhalb von Grenzen (#687)
- UnitConfig.Approvals: Operationen, Hoechstzahl je Tag, Stunden je Freigabe; nur der Mensch setzt sie (OrgAdministration.SetApprovals mit Audit, Teilmengenregel nach oben, Senken oben senkt unten mit; Agentenaenderungen/Sonderbudget nie freigebbar) - LeadApprovals: Vorlage geht an die naechste Leitung, deren Einheit die Operation erlaubt; nie eigene Aufrufe, nie eine Leitung mit Fremdinhalt, Tagesdeckel, Frist. Entscheidung ueber Escalation approve_call/reject_call, im Audit als lead:<id> - Der Mensch sieht alles und kann zuerst entscheiden; danach wird die Leitungsaufgabe geschlossen. Nach der Frist nur noch der Mensch. - StagedCalls: Approver, ApproverUntil, ApproverTaskId (Nachruestung) - WebUI: Hinweis "bei Leitung" in der Freigabestelle, Zaehler ohne diese, Quote je Leitung; Karte in der Organisationsansicht; i18n en/de - Tests: LeadApprovalsTests (13), OrgAdministrationTests (+2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dafb8241a0
commit
f51186269c
@@ -307,7 +307,13 @@ public sealed record StagedCallDto(
|
||||
DateTime CreatedAt,
|
||||
int PreviousApprovals = 0,
|
||||
bool CanAllowSimilar = false,
|
||||
bool SuggestAllowSimilar = false);
|
||||
bool SuggestAllowSimilar = false,
|
||||
// #687: the lead that may decide it instead of the human, and until when.
|
||||
string? WithLead = null,
|
||||
DateTime? LeadUntil = null);
|
||||
|
||||
/// <summary>#687: How a lead decided the approvals it was given.</summary>
|
||||
public sealed record LeadApprovalStatsDto(string LeadId, int Routed, int Approved, int Rejected, int Lapsed, double? ApprovalRate);
|
||||
|
||||
/// <summary>#641: Eine Dauerfreigabe - "Gleichartiges kuenftig erlauben".</summary>
|
||||
public sealed record StandingApprovalDto(
|
||||
|
||||
@@ -220,7 +220,10 @@ internal static class Mapping
|
||||
call.CreatedAt,
|
||||
bisherFreigegeben,
|
||||
erlaubbar,
|
||||
erlaubbar && bisherFreigegeben >= StagingService.VorschlagAb);
|
||||
erlaubbar && bisherFreigegeben >= StagingService.VorschlagAb,
|
||||
// #687: with a lead until then — the human can still decide it first.
|
||||
call.WithLead(DateTime.UtcNow) ? call.Approver : null,
|
||||
call.WithLead(DateTime.UtcNow) ? call.ApproverUntil : null);
|
||||
}
|
||||
|
||||
public static StandingApprovalDto ToDto(this Dauerfreigabe d)
|
||||
|
||||
@@ -18,7 +18,13 @@ public sealed record UnitDto(
|
||||
IReadOnlyList<string>? EffectiveToolCeiling,
|
||||
UnitDefaultsDto Defaults,
|
||||
string Path,
|
||||
int Depth);
|
||||
int Depth,
|
||||
// #687: what the lead may approve instead of the human; null: nothing.
|
||||
UnitApprovalsDto? Approvals = null);
|
||||
|
||||
public sealed record UnitApprovalsDto(IReadOnlyList<string> Operations, int PerDay, double Hours);
|
||||
|
||||
public sealed record ApprovalsInputDto(IReadOnlyList<string>? Operations, int? PerDay, double? Hours, string Reason = "");
|
||||
|
||||
public sealed record UnitDefaultsDto(string? Model, decimal? DailyCostUsd, long? DailyTokens, string? WhenExhausted);
|
||||
|
||||
|
||||
@@ -365,6 +365,15 @@ internal static class ApiEndpoints
|
||||
result.Kind is Core.Staging.StagingResultKind.Executed, result.Message));
|
||||
});
|
||||
|
||||
// #687: How often each lead approved what it was given — a lead that approves everything does nothing.
|
||||
staging.MapGet("/leads", async (AppHost host, CancellationToken ct, int days = 30) =>
|
||||
{
|
||||
if (host.Engine?.LeadApprovals is not { } leads)
|
||||
return Results.Ok(Array.Empty<LeadApprovalStatsDto>());
|
||||
var stats = await leads.StatsAsync(days, ct);
|
||||
return Results.Ok(stats.Select(x => new LeadApprovalStatsDto(x.LeadId, x.Routed, x.Approved, x.Rejected, x.Lapsed, x.ApprovalRate)).ToList());
|
||||
});
|
||||
|
||||
staging.MapGet("/standing", async (AppHost host, CancellationToken ct, bool revoked = false) =>
|
||||
{
|
||||
if (host.Staging is not { } service)
|
||||
|
||||
@@ -27,7 +27,8 @@ internal static class OrganisationEndpoints
|
||||
u.Id, u.Name, u.ParentId, u.LeaderAgentId, u.Goal, u.ToolCeiling,
|
||||
chart.ToolCeiling(u.Id)?.Order(StringComparer.OrdinalIgnoreCase).ToList(),
|
||||
new UnitDefaultsDto(u.Defaults.Model, u.Defaults.DailyCostUsd, u.Defaults.DailyTokens, u.Defaults.WhenExhausted),
|
||||
chart.PathOf(u.Id), chart.Depth(u.Id)))
|
||||
chart.PathOf(u.Id), chart.Depth(u.Id),
|
||||
u.Approvals is { } ap ? new UnitApprovalsDto(ap.Operations, ap.PerDay, ap.Hours) : null))
|
||||
.ToList();
|
||||
|
||||
var agents = host.Instance.Agents
|
||||
@@ -80,6 +81,10 @@ internal static class OrganisationEndpoints
|
||||
? Results.Ok(ToDto(host.Organisation.PreviewMoveAgent(agentId, input.Target)))
|
||||
: host.Organisation.MoveAgent(agentId, input.Target, input.Reason, input.DropExcessTools).ToResponse());
|
||||
|
||||
// #687: What the lead may approve instead of the human.
|
||||
org.MapPut("/units/{unitId}/approvals", (AppHost host, string unitId, ApprovalsInputDto input) =>
|
||||
host.Organisation.SetApprovals(unitId, input.Operations, input.PerDay, input.Hours, input.Reason).ToResponse());
|
||||
|
||||
org.MapPut("/units/{unitId}/ceiling", (AppHost host, string unitId, CeilingInputDto input) =>
|
||||
input.Preview
|
||||
? Results.Ok(ToDto(host.Organisation.PreviewToolCeiling(unitId, input.Tools)))
|
||||
|
||||
Reference in New Issue
Block a user