129 lines
4.6 KiB
C#
129 lines
4.6 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.Text.Json;
|
|
using ClawdDotNet.Core.Api.Models;
|
|
|
|
namespace ClawdDotNet.Core.Engine;
|
|
|
|
/// <summary>
|
|
/// Liest die Fehlerantworten, die die Engine selbst fuer Werkzeugaufrufe erzeugt:
|
|
/// <c>{"error": "..."}</c>.
|
|
///
|
|
/// <para>Erfolgreiche Werkzeugergebnisse kommen gerahmt an (#193) und beginnen deshalb nie
|
|
/// mit <c>{</c> - ein abgerufenes JSON mit einem Feld "error" wird so nicht mit einem
|
|
/// Fehlschlag verwechselt.</para>
|
|
/// </summary>
|
|
internal static class WerkzeugFehler
|
|
{
|
|
internal static string Json(string message) => JsonSerializer.Serialize(new { error = message });
|
|
|
|
internal static bool TryRead(string? result, [NotNullWhen(true)] out string? message)
|
|
{
|
|
message = null;
|
|
if (string.IsNullOrEmpty(result) || result[0] != '{')
|
|
return false;
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(result);
|
|
if (doc.RootElement.ValueKind == JsonValueKind.Object
|
|
&& doc.RootElement.TryGetProperty("error", out var error)
|
|
&& error.ValueKind == JsonValueKind.String)
|
|
{
|
|
message = error.GetString() ?? "";
|
|
return true;
|
|
}
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// Kein JSON - dann auch keine Fehlerantwort der Engine.
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Ein Lauf endet, weil derselbe Aufruf wiederholt gleich gescheitert ist (#581).</summary>
|
|
internal sealed class WiederholterFehlschlagException(string message) : Exception(message);
|
|
|
|
/// <summary>
|
|
/// Erkennt, wenn ein Agent denselben Werkzeugaufruf immer wieder gleich scheitern laesst
|
|
/// (#581, Punkt 4).
|
|
///
|
|
/// <para>Im Pruefdurchgang #572 schickte ein Agent denselben fehlschlagenden Aufruf bis zur
|
|
/// Schrittgrenze - jeder Versuch kostet den vollen Kontext. Beim zweiten gleichen
|
|
/// Fehlschlag bekommt das Modell deshalb zum Fehler den Hinweis, anders vorzugehen; beim
|
|
/// dritten endet der Lauf mit eigenem Status.</para>
|
|
///
|
|
/// <para>Gleich heisst: dasselbe Werkzeug, dieselben Argumente (ohne Leerraum-Unterschiede)
|
|
/// und derselbe Fehlertext. Gezaehlt wird je Aufruf ueber den ganzen Lauf, nicht nur direkt
|
|
/// hintereinander - sonst entkaeme ein Agent, der zwei kaputte Aufrufe abwechselt. Gelingt
|
|
/// derselbe Aufruf, beginnt seine Zaehlung von vorn.</para>
|
|
/// </summary>
|
|
internal sealed class FehlschlagWache
|
|
{
|
|
internal const int HinweisAb = 2;
|
|
internal const int AbbruchAb = 3;
|
|
|
|
internal const string Hinweis =
|
|
"Dieser Aufruf ist mit denselben Argumenten bereits auf dieselbe Weise gescheitert. "
|
|
+ "Nicht noch einmal gleich aufrufen - anders vorgehen (andere Argumente, anderes Werkzeug) "
|
|
+ "oder mit einer Erklaerung abschliessen.";
|
|
|
|
private readonly Dictionary<string, (string Fehler, int Anzahl)> _stand = new(StringComparer.Ordinal);
|
|
|
|
/// <summary>
|
|
/// Prueft die Ergebnisse eines Zuges. Ergaenzt beim zweiten gleichen Fehlschlag das
|
|
/// Ergebnis um den Hinweis (an Ort und Stelle) und liefert beim dritten den Grund fuer
|
|
/// den Abbruch - sonst <c>null</c>.
|
|
/// </summary>
|
|
internal string? Pruefe(IReadOnlyList<ToolCall> calls, string?[] results)
|
|
{
|
|
string? abbruch = null;
|
|
|
|
for (var i = 0; i < calls.Count && i < results.Length; i++)
|
|
{
|
|
var name = calls[i].Function.Name;
|
|
var schluessel = name + "\0" + Normalisiere(calls[i].Function.Arguments);
|
|
|
|
if (!WerkzeugFehler.TryRead(results[i], out var fehler))
|
|
{
|
|
if (results[i] is not null)
|
|
_stand.Remove(schluessel);
|
|
continue;
|
|
}
|
|
|
|
var anzahl = _stand.TryGetValue(schluessel, out var bisher) && bisher.Fehler == fehler
|
|
? bisher.Anzahl + 1
|
|
: 1;
|
|
_stand[schluessel] = (fehler, anzahl);
|
|
|
|
if (anzahl >= AbbruchAb)
|
|
{
|
|
abbruch ??= $"Werkzeug '{name}' ist {anzahl}-mal mit denselben Argumenten gescheitert: {fehler}";
|
|
}
|
|
else if (anzahl >= HinweisAb)
|
|
{
|
|
results[i] = JsonSerializer.Serialize(new { error = fehler, hinweis = Hinweis });
|
|
}
|
|
}
|
|
|
|
return abbruch;
|
|
}
|
|
|
|
private static string Normalisiere(string? arguments)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(arguments))
|
|
return "";
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(arguments);
|
|
return JsonSerializer.Serialize(doc.RootElement);
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return arguments.Trim();
|
|
}
|
|
}
|
|
}
|