codex wip
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AnotherReplayReader.Utils
|
||||
{
|
||||
internal enum AIValidationSeverity
|
||||
{
|
||||
Info,
|
||||
WeakEvidence,
|
||||
Warning,
|
||||
Contradiction,
|
||||
Fatal
|
||||
}
|
||||
|
||||
internal enum AIValidationIssueKind
|
||||
{
|
||||
InvalidMachineReadableClaims,
|
||||
MissingMachineReadableClaims,
|
||||
MissingAlternative,
|
||||
WeakEvidence,
|
||||
MissingEvidence,
|
||||
InvalidEvidenceLevel,
|
||||
UnitCapabilityContradiction,
|
||||
UnitTimelineContradiction,
|
||||
UnsupportedGameKnowledge
|
||||
}
|
||||
|
||||
internal enum AIEvidenceLevel
|
||||
{
|
||||
Confirmed,
|
||||
HighlyLikely,
|
||||
Possible,
|
||||
Uncertain,
|
||||
RuledOut
|
||||
}
|
||||
|
||||
internal sealed record AIUnitClaim(
|
||||
string UnitId,
|
||||
string Player,
|
||||
string Claim,
|
||||
AIEvidenceLevel EvidenceLevel,
|
||||
ImmutableArray<string> Evidence,
|
||||
ImmutableArray<string> Alternatives,
|
||||
ImmutableArray<string> NeedsConfirmation);
|
||||
|
||||
internal sealed record AIEventClaim(
|
||||
string Claim,
|
||||
AIEvidenceLevel EvidenceLevel,
|
||||
ImmutableArray<string> Evidence);
|
||||
|
||||
internal sealed record AITimelineClaim(
|
||||
string Claim,
|
||||
AIEvidenceLevel EvidenceLevel,
|
||||
ImmutableArray<string> Evidence);
|
||||
|
||||
internal sealed record AIMachineReadableClaims(
|
||||
ImmutableArray<AIUnitClaim> UnitClaims,
|
||||
ImmutableArray<AIEventClaim> EventClaims,
|
||||
ImmutableArray<AITimelineClaim> TimelineClaims)
|
||||
{
|
||||
public static AIMachineReadableClaims Empty { get; } = new(
|
||||
ImmutableArray<AIUnitClaim>.Empty,
|
||||
ImmutableArray<AIEventClaim>.Empty,
|
||||
ImmutableArray<AITimelineClaim>.Empty);
|
||||
}
|
||||
|
||||
internal sealed record AIValidationIssue(
|
||||
AIValidationSeverity Severity,
|
||||
AIValidationIssueKind Kind,
|
||||
string Message,
|
||||
string? UnitId = null,
|
||||
TimeSpan? Time = null);
|
||||
|
||||
internal sealed record AIValidationResult(
|
||||
AIMachineReadableClaims Claims,
|
||||
ImmutableArray<AIValidationIssue> Issues)
|
||||
{
|
||||
public static AIValidationResult Empty { get; } =
|
||||
new(AIMachineReadableClaims.Empty, ImmutableArray<AIValidationIssue>.Empty);
|
||||
|
||||
public bool RequiresRevision =>
|
||||
Issues.Any(i => i.Severity is AIValidationSeverity.Contradiction or AIValidationSeverity.Fatal);
|
||||
|
||||
public bool HasIssues => !Issues.IsEmpty;
|
||||
}
|
||||
|
||||
internal static class AIAnalysisValidation
|
||||
{
|
||||
private static readonly Regex _jsonFenceRegex = new(
|
||||
@"```(?:json)?\s*(\{[\s\S]*?\})\s*```",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
public static AIValidationResult ValidateMachineReadableClaims(string response)
|
||||
{
|
||||
var issues = ImmutableArray.CreateBuilder<AIValidationIssue>();
|
||||
var json = ExtractJsonObject(response);
|
||||
if (json is null || string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Warning,
|
||||
AIValidationIssueKind.MissingMachineReadableClaims,
|
||||
"AI 未输出机器可读声明,无法进行自动验证。"));
|
||||
return new AIValidationResult(AIMachineReadableClaims.Empty, issues.ToImmutable());
|
||||
}
|
||||
|
||||
var claims = ParseClaims(json, issues);
|
||||
if (claims is null)
|
||||
{
|
||||
return new AIValidationResult(AIMachineReadableClaims.Empty, issues.ToImmutable());
|
||||
}
|
||||
|
||||
ValidateClaimSelfConsistency(claims, issues);
|
||||
return new AIValidationResult(claims, issues.ToImmutable());
|
||||
}
|
||||
|
||||
public static string FormatIssues(ImmutableArray<AIValidationIssue> issues)
|
||||
{
|
||||
if (issues.IsEmpty)
|
||||
{
|
||||
return "未发现机器可读声明问题。";
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (var issue in issues)
|
||||
{
|
||||
var unitText = string.IsNullOrWhiteSpace(issue.UnitId)
|
||||
? string.Empty
|
||||
: $" UnitId={issue.UnitId}";
|
||||
sb.AppendLine($"[{issue.Severity}/{issue.Kind}]{unitText} {issue.Message}");
|
||||
}
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
private static string? ExtractJsonObject(string response)
|
||||
{
|
||||
var markerIndex = response.LastIndexOf("[机器可读声明]", StringComparison.OrdinalIgnoreCase);
|
||||
var searchText = markerIndex >= 0 ? response.Substring(markerIndex) : response;
|
||||
|
||||
var matches = _jsonFenceRegex.Matches(searchText);
|
||||
if (matches.Count > 0)
|
||||
{
|
||||
return matches[matches.Count - 1].Groups[1].Value;
|
||||
}
|
||||
|
||||
var start = searchText.LastIndexOf('{');
|
||||
var end = searchText.LastIndexOf('}');
|
||||
if (start >= 0 && end > start)
|
||||
{
|
||||
return searchText.Substring(start, end - start + 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static AIMachineReadableClaims? ParseClaims(
|
||||
string json,
|
||||
ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json, new JsonDocumentOptions
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
CommentHandling = JsonCommentHandling.Skip
|
||||
});
|
||||
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Warning,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
"机器可读声明不是 JSON object。"));
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AIMachineReadableClaims(
|
||||
ReadUnitClaims(root),
|
||||
ReadSimpleClaims(root, "eventClaims")
|
||||
.Select(c => new AIEventClaim(c.Claim, c.EvidenceLevel, c.Evidence))
|
||||
.ToImmutableArray(),
|
||||
ReadSimpleClaims(root, "timelineClaims")
|
||||
.Select(c => new AITimelineClaim(c.Claim, c.EvidenceLevel, c.Evidence))
|
||||
.ToImmutableArray());
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Warning,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
$"机器可读声明 JSON 解析失败:{ex.Message}"));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static ImmutableArray<AIUnitClaim> ReadUnitClaims(JsonElement root)
|
||||
{
|
||||
if (!TryGetArray(root, "unitClaims", out var unitClaims))
|
||||
{
|
||||
return ImmutableArray<AIUnitClaim>.Empty;
|
||||
}
|
||||
|
||||
var result = ImmutableArray.CreateBuilder<AIUnitClaim>();
|
||||
foreach (var item in unitClaims.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new AIUnitClaim(
|
||||
ReadFlexibleString(item, "unitId"),
|
||||
ReadFlexibleString(item, "player"),
|
||||
ReadFlexibleString(item, "claim"),
|
||||
ReadEvidenceLevel(item),
|
||||
ReadStringArray(item, "evidence"),
|
||||
ReadStringArray(item, "alternatives"),
|
||||
ReadStringArray(item, "needsConfirmation")));
|
||||
}
|
||||
return result.ToImmutable();
|
||||
}
|
||||
|
||||
private sealed record SimpleClaim(
|
||||
string Claim,
|
||||
AIEvidenceLevel EvidenceLevel,
|
||||
ImmutableArray<string> Evidence);
|
||||
|
||||
private static ImmutableArray<SimpleClaim> ReadSimpleClaims(JsonElement root, string propertyName)
|
||||
{
|
||||
if (!TryGetArray(root, propertyName, out var claims))
|
||||
{
|
||||
return ImmutableArray<SimpleClaim>.Empty;
|
||||
}
|
||||
|
||||
var result = ImmutableArray.CreateBuilder<SimpleClaim>();
|
||||
foreach (var item in claims.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new SimpleClaim(
|
||||
ReadFlexibleString(item, "claim"),
|
||||
ReadEvidenceLevel(item),
|
||||
ReadStringArray(item, "evidence")));
|
||||
}
|
||||
return result.ToImmutable();
|
||||
}
|
||||
|
||||
private static void ValidateClaimSelfConsistency(
|
||||
AIMachineReadableClaims claims,
|
||||
ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
foreach (var claim in claims.UnitClaims)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(claim.UnitId))
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Warning,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
"Unit claim 缺少 unitId。"));
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(claim.Claim))
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Warning,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
"Unit claim 缺少 claim。",
|
||||
claim.UnitId));
|
||||
}
|
||||
if (claim.EvidenceLevel is AIEvidenceLevel.Confirmed or AIEvidenceLevel.HighlyLikely
|
||||
&& claim.Evidence.IsEmpty)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Warning,
|
||||
AIValidationIssueKind.MissingEvidence,
|
||||
"高置信 UnitId 推测缺少 evidence。",
|
||||
claim.UnitId));
|
||||
}
|
||||
if (claim.EvidenceLevel is AIEvidenceLevel.Possible or AIEvidenceLevel.Uncertain
|
||||
&& claim.Alternatives.IsEmpty
|
||||
&& claim.NeedsConfirmation.IsEmpty)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.WeakEvidence,
|
||||
AIValidationIssueKind.MissingAlternative,
|
||||
"低置信 UnitId 推测应提供 alternatives 或 needsConfirmation。",
|
||||
claim.UnitId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetArray(JsonElement root, string propertyName, out JsonElement array)
|
||||
{
|
||||
if (root.TryGetProperty(propertyName, out array)
|
||||
&& array.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
array = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static AIEvidenceLevel ReadEvidenceLevel(JsonElement item)
|
||||
{
|
||||
var value = ReadFlexibleString(item, "evidenceLevel");
|
||||
return NormalizeEvidenceLevel(value);
|
||||
}
|
||||
|
||||
private static AIEvidenceLevel NormalizeEvidenceLevel(string value)
|
||||
{
|
||||
value = value.Trim().Replace("_", "").Replace("-", "").Replace(" ", "");
|
||||
return value.ToLowerInvariant() switch
|
||||
{
|
||||
"confirmed" or "确定" => AIEvidenceLevel.Confirmed,
|
||||
"highlylikely" or "high" or "高度可能" => AIEvidenceLevel.HighlyLikely,
|
||||
"possible" or "可能" => AIEvidenceLevel.Possible,
|
||||
"ruledout" or "excluded" or "已排除" => AIEvidenceLevel.RuledOut,
|
||||
_ => AIEvidenceLevel.Uncertain,
|
||||
};
|
||||
}
|
||||
|
||||
private static string ReadFlexibleString(JsonElement item, string propertyName)
|
||||
{
|
||||
if (!item.TryGetProperty(propertyName, out var value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => value.GetString() ?? string.Empty,
|
||||
JsonValueKind.Number => value.GetRawText(),
|
||||
JsonValueKind.True => "true",
|
||||
JsonValueKind.False => "false",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> ReadStringArray(JsonElement item, string propertyName)
|
||||
{
|
||||
if (!item.TryGetProperty(propertyName, out var value))
|
||||
{
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
if (value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var text = value.GetString();
|
||||
return string.IsNullOrWhiteSpace(text)
|
||||
? ImmutableArray<string>.Empty
|
||||
: ImmutableArray.Create(text!);
|
||||
}
|
||||
|
||||
if (value.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
var result = new List<string>();
|
||||
foreach (var element in value.EnumerateArray())
|
||||
{
|
||||
var text = element.ValueKind == JsonValueKind.String
|
||||
? element.GetString()
|
||||
: element.GetRawText();
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
result.Add(text!);
|
||||
}
|
||||
}
|
||||
return result.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user