已把工具调用从 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
This commit is contained in:
@@ -0,0 +1,516 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user