Files
WulaFallenEmpireRW/Source/WulaFallenEmpire/EventSystem/AI/Utils/JsonToolCallParser.cs
ProjectKoi-Kalo\Kalo b906a468b6 已把工具调用从 XML 改成 OpenAI 兼容 JSON,并统一解析/执行流程。改动概览如下:
新增 JSON tool_calls 解析/序列化并替换核心执行与提示词为 JSON-only:JsonToolCallParser.cs、AIIntelligenceCore.cs
工具基类移除 XML 解析,统一 JSON 参数读取与类型转换辅助:AITool.cs
工具实现统一 JSON args/UsageSchema(含重写/修复):Tool_ModifyGoodwill.cs、Tool_SendReinforcement.cs、Tool_GetMapPawns.cs、Tool_GetMapResources.cs、Tool_GetAvailablePrefabs.cs、Tool_CallPrefabAirdrop.cs、Tool_CallBombardment.cs、Tool_GetAvailableBombardments.cs、Tool_GetPawnStatus.cs、Tool_GetRecentNotifications.cs、Tool_SearchThingDef.cs、Tool_SearchPawnKind.cs、Tool_ChangeExpression.cs、Tool_SetOverwatchMode.cs、Tool_RememberFact.cs、Tool_RecallMemories.cs、Tool_SpawnResources.cs、Tool_AnalyzeScreen.cs
轰炸相关解析统一到 JSON 字典并增强数值解析:BombardmentUtility.cs
UI 对话展示改为剥离 JSON tool_calls:Overlay_WulaLink.cs、Dialog_AIConversation.cs
2025-12-31 01:45:38 +08:00

517 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace WulaFallenEmpire.EventSystem.AI.Utils
{
public sealed class ToolCallInfo
{
public string Id;
public string Name;
public Dictionary<string, object> Arguments;
public string ArgumentsJson;
}
public static class JsonToolCallParser
{
public static bool TryParseToolCalls(string input, out List<ToolCallInfo> toolCalls)
{
toolCalls = null;
if (string.IsNullOrWhiteSpace(input)) return false;
if (!TryParseValue(input, out object root)) return false;
if (root is not Dictionary<string, object> obj) return false;
if (!TryGetValue(obj, "tool_calls", out object callsObj)) return false;
if (callsObj is not List<object> callsList) return false;
var parsedCalls = new List<ToolCallInfo>();
foreach (var entry in callsList)
{
if (entry is not Dictionary<string, object> callObj) continue;
string id = TryGetString(callObj, "id");
string name = null;
object argsObj = null;
if (TryGetValue(callObj, "function", out object fnObj) && fnObj is Dictionary<string, object> fnDict)
{
name = TryGetString(fnDict, "name");
TryGetValue(fnDict, "arguments", out argsObj);
}
else
{
name = TryGetString(callObj, "name");
TryGetValue(callObj, "arguments", out argsObj);
}
if (string.IsNullOrWhiteSpace(name)) continue;
if (!TryNormalizeArguments(argsObj, out Dictionary<string, object> args, out string argsJson))
{
args = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
argsJson = "{}";
}
parsedCalls.Add(new ToolCallInfo
{
Id = id,
Name = name.Trim(),
Arguments = args,
ArgumentsJson = argsJson
});
}
toolCalls = parsedCalls;
return true;
}
public static bool TryParseToolCallsFromText(string input, out List<ToolCallInfo> toolCalls, out string jsonFragment)
{
toolCalls = null;
jsonFragment = null;
if (string.IsNullOrWhiteSpace(input)) return false;
string trimmed = input.Trim();
if (TryParseToolCalls(trimmed, out toolCalls))
{
jsonFragment = trimmed;
return true;
}
int firstBrace = trimmed.IndexOf('{');
int lastBrace = trimmed.LastIndexOf('}');
if (firstBrace >= 0 && lastBrace > firstBrace)
{
string candidate = trimmed.Substring(firstBrace, lastBrace - firstBrace + 1);
if (TryParseToolCalls(candidate, out toolCalls))
{
jsonFragment = candidate;
return true;
}
}
return false;
}
public static bool TryParseObject(string json, out Dictionary<string, object> obj)
{
obj = null;
if (string.IsNullOrWhiteSpace(json)) return false;
if (!TryParseValue(json, out object value)) return false;
if (value is not Dictionary<string, object> dict) return false;
obj = dict;
return true;
}
public static bool LooksLikeJson(string text)
{
if (string.IsNullOrWhiteSpace(text)) return false;
string trimmed = text.TrimStart();
return trimmed.StartsWith("{", StringComparison.Ordinal) || trimmed.StartsWith("[", StringComparison.Ordinal);
}
public static string SerializeToJson(object value)
{
var sb = new StringBuilder();
AppendValue(sb, value);
return sb.ToString();
}
private static void AppendValue(StringBuilder sb, object value)
{
if (value == null)
{
sb.Append("null");
return;
}
if (value is string s)
{
sb.Append('\"').Append(EscapeJson(s)).Append('\"');
return;
}
if (value is bool b)
{
sb.Append(b ? "true" : "false");
return;
}
if (value is double d)
{
sb.Append(d.ToString("0.################", CultureInfo.InvariantCulture));
return;
}
if (value is float f)
{
sb.Append(f.ToString("0.################", CultureInfo.InvariantCulture));
return;
}
if (value is int or long or short or byte)
{
sb.Append(Convert.ToString(value, CultureInfo.InvariantCulture));
return;
}
if (value is Dictionary<string, object> obj)
{
sb.Append('{');
bool first = true;
foreach (var kvp in obj)
{
if (!first) sb.Append(',');
first = false;
sb.Append('\"').Append(EscapeJson(kvp.Key)).Append('\"').Append(':');
AppendValue(sb, kvp.Value);
}
sb.Append('}');
return;
}
if (value is List<object> list)
{
sb.Append('[');
for (int i = 0; i < list.Count; i++)
{
if (i > 0) sb.Append(',');
AppendValue(sb, list[i]);
}
sb.Append(']');
return;
}
sb.Append('\"').Append(EscapeJson(Convert.ToString(value, CultureInfo.InvariantCulture) ?? "")).Append('\"');
}
private static bool TryNormalizeArguments(object argsObj, out Dictionary<string, object> args, out string argsJson)
{
args = null;
argsJson = null;
if (argsObj == null)
{
args = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
argsJson = "{}";
return true;
}
if (argsObj is Dictionary<string, object> dict)
{
args = dict;
argsJson = SerializeToJson(dict);
return true;
}
if (argsObj is string s)
{
if (string.IsNullOrWhiteSpace(s))
{
args = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
argsJson = "{}";
return true;
}
if (TryParseObject(s, out Dictionary<string, object> parsed))
{
args = parsed;
argsJson = SerializeToJson(parsed);
return true;
}
return false;
}
return false;
}
private static bool TryParseValue(string json, out object value)
{
value = null;
var reader = new JsonReader(json);
if (!reader.TryReadValue(out value)) return false;
reader.SkipWhitespace();
return reader.IsAtEnd;
}
private static string EscapeJson(string value)
{
if (string.IsNullOrEmpty(value)) return "";
var sb = new StringBuilder();
foreach (char c in value)
{
switch (c)
{
case '\\': sb.Append("\\\\"); break;
case '\"': sb.Append("\\\""); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
default:
if (c < 0x20)
{
sb.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
}
else
{
sb.Append(c);
}
break;
}
}
return sb.ToString();
}
private static bool TryGetValue(Dictionary<string, object> obj, string key, out object value)
{
if (obj == null)
{
value = null;
return false;
}
foreach (var kvp in obj)
{
if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
{
value = kvp.Value;
return true;
}
}
value = null;
return false;
}
private static string TryGetString(Dictionary<string, object> obj, string key)
{
if (TryGetValue(obj, key, out object value) && value != null)
{
return Convert.ToString(value, CultureInfo.InvariantCulture);
}
return null;
}
private sealed class JsonReader
{
private readonly string _text;
private int _index;
public JsonReader(string text)
{
_text = text ?? "";
_index = 0;
}
public bool IsAtEnd => _index >= _text.Length;
public void SkipWhitespace()
{
while (_index < _text.Length && char.IsWhiteSpace(_text[_index]))
{
_index++;
}
}
public bool TryReadValue(out object value)
{
value = null;
SkipWhitespace();
if (IsAtEnd) return false;
char c = _text[_index];
if (c == '{') return TryReadObject(out value);
if (c == '[') return TryReadArray(out value);
if (c == '\"') return TryReadString(out value);
if (c == '-' || char.IsDigit(c)) return TryReadNumber(out value);
if (TryReadLiteral("true")) { value = true; return true; }
if (TryReadLiteral("false")) { value = false; return true; }
if (TryReadLiteral("null")) { value = null; return true; }
return false;
}
private bool TryReadObject(out object value)
{
value = null;
if (!TryReadChar('{')) return false;
SkipWhitespace();
var dict = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
if (TryReadChar('}'))
{
value = dict;
return true;
}
while (true)
{
SkipWhitespace();
if (!TryReadString(out object keyObj)) return false;
string key = keyObj as string ?? "";
SkipWhitespace();
if (!TryReadChar(':')) return false;
if (!TryReadValue(out object itemValue)) return false;
dict[key] = itemValue;
SkipWhitespace();
if (TryReadChar('}'))
{
value = dict;
return true;
}
if (!TryReadChar(',')) return false;
}
}
private bool TryReadArray(out object value)
{
value = null;
if (!TryReadChar('[')) return false;
SkipWhitespace();
var list = new List<object>();
if (TryReadChar(']'))
{
value = list;
return true;
}
while (true)
{
if (!TryReadValue(out object item)) return false;
list.Add(item);
SkipWhitespace();
if (TryReadChar(']'))
{
value = list;
return true;
}
if (!TryReadChar(',')) return false;
}
}
private bool TryReadString(out object value)
{
value = null;
if (!TryReadChar('\"')) return false;
var sb = new StringBuilder();
while (_index < _text.Length)
{
char c = _text[_index++];
if (c == '\"')
{
value = sb.ToString();
return true;
}
if (c == '\\')
{
if (_index >= _text.Length) return false;
char esc = _text[_index++];
switch (esc)
{
case '\"': sb.Append('\"'); break;
case '\\': sb.Append('\\'); break;
case '/': sb.Append('/'); break;
case 'b': sb.Append('\b'); break;
case 'f': sb.Append('\f'); break;
case 'n': sb.Append('\n'); break;
case 'r': sb.Append('\r'); break;
case 't': sb.Append('\t'); break;
case 'u':
if (_index + 4 > _text.Length) return false;
string hex = _text.Substring(_index, 4);
if (!int.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int code))
{
return false;
}
sb.Append((char)code);
_index += 4;
break;
default:
return false;
}
}
else
{
sb.Append(c);
}
}
return false;
}
private bool TryReadNumber(out object value)
{
value = null;
int start = _index;
if (_text[_index] == '-') _index++;
while (_index < _text.Length && char.IsDigit(_text[_index]))
{
_index++;
}
bool hasDot = false;
if (_index < _text.Length && _text[_index] == '.')
{
hasDot = true;
_index++;
while (_index < _text.Length && char.IsDigit(_text[_index]))
{
_index++;
}
}
if (_index < _text.Length && (_text[_index] == 'e' || _text[_index] == 'E'))
{
hasDot = true;
_index++;
if (_index < _text.Length && (_text[_index] == '+' || _text[_index] == '-'))
{
_index++;
}
while (_index < _text.Length && char.IsDigit(_text[_index]))
{
_index++;
}
}
string number = _text.Substring(start, _index - start);
if (!hasDot && long.TryParse(number, NumberStyles.Integer, CultureInfo.InvariantCulture, out long longVal))
{
value = longVal;
return true;
}
if (double.TryParse(number, NumberStyles.Float, CultureInfo.InvariantCulture, out double dbl))
{
value = dbl;
return true;
}
return false;
}
private bool TryReadLiteral(string literal)
{
SkipWhitespace();
if (_text.Length - _index < literal.Length) return false;
if (string.Compare(_text, _index, literal, 0, literal.Length, StringComparison.OrdinalIgnoreCase) != 0)
{
return false;
}
_index += literal.Length;
return true;
}
private bool TryReadChar(char expected)
{
SkipWhitespace();
if (_index >= _text.Length) return false;
if (_text[_index] != expected) return false;
_index++;
return true;
}
}
}
}