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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+67
-3
@@ -17,13 +17,51 @@ namespace AnotherReplayReader.Utils
|
||||
{
|
||||
internal sealed class AIAnalyze
|
||||
{
|
||||
public static string GetSystemPrompt(Replay replay, ImmutableSortedDictionary<int, Player> players)
|
||||
public static string GetSystemPrompt(
|
||||
Replay replay,
|
||||
ImmutableSortedDictionary<int, Player> players,
|
||||
AiPromptSettings? promptSettings = null)
|
||||
{
|
||||
var defaultPrompt = BuildDefaultSystemPrompt(replay, players);
|
||||
return ComposeSystemPrompt(defaultPrompt, promptSettings);
|
||||
}
|
||||
|
||||
public static string ComposeSystemPrompt(string defaultPrompt, AiPromptSettings? promptSettings)
|
||||
{
|
||||
var customSystemPrompt = promptSettings?.CustomSystemPrompt ?? string.Empty;
|
||||
if (promptSettings?.UseCustomSystemPrompt == true
|
||||
&& !string.IsNullOrWhiteSpace(customSystemPrompt))
|
||||
{
|
||||
defaultPrompt = customSystemPrompt;
|
||||
}
|
||||
|
||||
var additionalRules = promptSettings?.AdditionalRules ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(additionalRules))
|
||||
{
|
||||
defaultPrompt = defaultPrompt.TrimEnd()
|
||||
+ "\n\n# 用户自定义补充规则\n"
|
||||
+ additionalRules.Trim();
|
||||
}
|
||||
|
||||
return defaultPrompt.Replace("\r", "");
|
||||
}
|
||||
|
||||
public static string BuildDefaultSystemPrompt(Replay replay, ImmutableSortedDictionary<int, Player> players)
|
||||
{
|
||||
|
||||
var generalDescriptions = @"
|
||||
你是一位 RTS 游戏数据分析师,你擅长从大量数据中发现有趣的规律和细节。
|
||||
用户则是一位玩家,用户会向你提供玩家操作记录,你要对其进行分析。
|
||||
|
||||
# 核心原则
|
||||
- 你只能根据用户提供的操作记录、玩家信息、下方游戏规则和明确给出的背景知识进行分析。
|
||||
- 不要使用现实世界常识或其他 RTS 游戏常识覆盖这里的游戏设定。例如:步兵、直升机、建筑水陆摆放、运输能力、两栖能力都必须以这里的规则和单位描述为准。
|
||||
- 不确定时必须保留多个候选,不要为了让解说流畅而过早下定论。
|
||||
- 对 UnitId、单位类型、战术意图的判断必须区分证据等级:确定、高度可能、可能、待确认、已排除。
|
||||
- 每个关键推理都应当包含支持证据;如果存在会推翻该推理的反证,也要主动指出。
|
||||
- 如果某个技能或行为可以对应多个单位,先列出候选,并说明还需要哪些后续迹象才能确认。
|
||||
- 对已经被操作记录直接否定的判断必须修正或放弃,不要坚持原结论。
|
||||
|
||||
# 输入格式
|
||||
## 用户初始输入
|
||||
- 玩家信息
|
||||
@@ -83,11 +121,36 @@ namespace AnotherReplayReader.Utils
|
||||
- 也可以重点关注PlayerTech、英雄、工程师
|
||||
- 按照**推理指南**进行详细的思考与推理,列举你的推理与发现
|
||||
- 输出:该阶段的各个主要事件,以及你的推理和发现
|
||||
- 假如推测 UnitId 对应的单位,请在正文中自然描述,并在末尾输出机器可读声明,方便程序验证
|
||||
|
||||
## 3. 最终总结阶段
|
||||
触发条件:用户输入包含:""请对以上内容进行总结""
|
||||
- 输出:所有分析的总结,以及这次对局的完整介绍
|
||||
|
||||
# 机器可读声明
|
||||
仅限于:分段分析阶段(第2阶段)
|
||||
如果你对 UnitId、关键事件或时间线做出了可验证推测,请在回答末尾附加下面格式。
|
||||
必须先输出一行`[机器可读声明]`,然后输出一个 JSON 代码块:
|
||||
[机器可读声明]
|
||||
```json
|
||||
{
|
||||
""unitClaims"": [
|
||||
{
|
||||
""unitId"": 123,
|
||||
""player"": ""PlayerA"",
|
||||
""claim"": ""AlliedMCV"",
|
||||
""evidenceLevel"": ""possible"",
|
||||
""evidence"": [""8:30 使用 SpecialPower_UnpackReplaceSelf""],
|
||||
""alternatives"": [""AlliedMiner 展开后的指挥中心""],
|
||||
""needsConfirmation"": [""是否曾使用 SpecialPower_PackReplaceSelf"", ""后续是否作为建造者出现""]
|
||||
}
|
||||
],
|
||||
""eventClaims"": [],
|
||||
""timelineClaims"": []
|
||||
}
|
||||
```
|
||||
如果没有可验证推测,可以输出空数组。不要在 JSON 里写注释。
|
||||
|
||||
# 推理指南
|
||||
推理需要分成多个阶段
|
||||
1. 观察
|
||||
@@ -953,6 +1016,7 @@ PlayerA: 开始出兵
|
||||
请按照按照[观察]、[分析]、[推理]、[进一步思考(可选)]的步骤,对各个事件进行分析和推理。
|
||||
|
||||
假如当前阶段存在一些较为重要的单位、而且能够推测出它们可能是什么单位,则可以列出单位的UnitId以及你对单位的推测
|
||||
如果你列出了 UnitId 推测、关键事件推测或时间线推测,请在回答末尾附加`[机器可读声明]` JSON 代码块;如果没有相关推测,则输出空数组。
|
||||
";
|
||||
return instruction.Trim().Replace("\r", "");
|
||||
}
|
||||
@@ -1273,8 +1337,8 @@ PlayerA: 开始出兵
|
||||
result.ReasoningTokens = GetIntegerProperty(usage, "reasoning_tokens") ?? result.ReasoningTokens;
|
||||
}
|
||||
|
||||
// 提取 usage(如果存在)
|
||||
if (doc.RootElement.TryGetProperty("error", out var error) && usage.ValueKind == JsonValueKind.Object)
|
||||
// 提取 error(如果存在)
|
||||
if (doc.RootElement.TryGetProperty("error", out var error) && error.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (error.TryGetProperty("message", out var message))
|
||||
{
|
||||
|
||||
+14
-2
@@ -18,11 +18,21 @@ namespace AnotherReplayReader
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
public List<AiModel> Models { get; set; } = [];
|
||||
|
||||
public double DefaultTemperature { get; set; } = 0.75;
|
||||
public double DefaultTemperature { get; set; } = 0.35;
|
||||
public double DefaultTopP { get; set; } = 0.95;
|
||||
public int DefaultMaxTokens { get; set; } = 16384;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI 分析提示词配置。
|
||||
/// </summary>
|
||||
public class AiPromptSettings
|
||||
{
|
||||
public bool UseCustomSystemPrompt { get; set; }
|
||||
public string CustomSystemPrompt { get; set; } = string.Empty;
|
||||
public string AdditionalRules { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模型条目
|
||||
/// </summary>
|
||||
@@ -270,6 +280,7 @@ namespace AnotherReplayReader
|
||||
public class AiSettings
|
||||
{
|
||||
public List<AiProvider> Providers { get; set; } = [];
|
||||
public AiPromptSettings Prompt { get; set; } = new();
|
||||
|
||||
// 以下两个不持久化,由 UI 层维护当前选中项
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
@@ -289,6 +300,7 @@ namespace AnotherReplayReader
|
||||
var settings = JsonSerializer.Deserialize<AiSettings>(json);
|
||||
if (settings is { } value && value.Providers.Count > 0)
|
||||
{
|
||||
value.Prompt ??= new AiPromptSettings();
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
@@ -342,4 +354,4 @@ namespace AnotherReplayReader
|
||||
File.WriteAllText(ConfigPath, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user