已把工具调用从 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:
@@ -10,6 +10,7 @@ using UnityEngine;
|
||||
using Verse;
|
||||
using WulaFallenEmpire;
|
||||
using WulaFallenEmpire.EventSystem.AI.Tools;
|
||||
using WulaFallenEmpire.EventSystem.AI.Utils;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI
|
||||
{
|
||||
@@ -95,20 +96,18 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
|
||||
private const string ToolRulesInstruction = @"
|
||||
# TOOL USE RULES
|
||||
1. **FORMATTING**: Tool calls MUST use the specified XML format. The tool name is the root tag, and each parameter is a child tag.
|
||||
<tool_name>
|
||||
<parameter_name>value</parameter_name>
|
||||
</tool_name>
|
||||
1. **FORMATTING**: Tool calls MUST be valid JSON using the following schema:
|
||||
{ ""tool_calls"": [ { ""type"": ""function"", ""function"": { ""name"": ""tool_name"", ""arguments"": { ... } } } ] }
|
||||
2. **STRICT OUTPUT**:
|
||||
- Your output MUST be either:
|
||||
- One or more XML tool calls (no extra text), OR
|
||||
- Exactly: <no_action/>
|
||||
- A JSON object with ""tool_calls"" (may be empty), OR
|
||||
- Exactly: { ""tool_calls"": [] }
|
||||
Do NOT include any natural language, explanation, markdown, or additional commentary.
|
||||
3. **MULTI-REQUEST RULE**:
|
||||
- If the user requests multiple items or information, you MUST output ALL required tool calls in the SAME tool-phase response.
|
||||
- Do NOT split multi-item requests across turns.
|
||||
4. **TOOLS**: You MAY call any tools listed in ""# TOOLS (AVAILABLE)"".
|
||||
5. **ANTI-HALLUCINATION**: Never invent tools, parameters, defNames, coordinates, or tool results. If a tool is needed but not available, use <no_action/> and proceed to the next phase.";
|
||||
5. **ANTI-HALLUCINATION**: Never invent tools, parameters, defNames, coordinates, or tool results. If a tool is needed but not available, output { ""tool_calls"": [] } and proceed to the next phase.";
|
||||
|
||||
public AIIntelligenceCore(World world) : base(world)
|
||||
{
|
||||
@@ -668,11 +667,11 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
if (!toolsEnabled)
|
||||
{
|
||||
return $"{fullInstruction}\n{goodwillContext}\nIMPORTANT: You MUST reply in the following language: {language}.\n" +
|
||||
"IMPORTANT: Tool calls are DISABLED in this turn. Reply in natural language only. Do NOT output any XML. " +
|
||||
"IMPORTANT: Tool calls are DISABLED in this turn. Reply in natural language only. Do NOT output any tool call JSON. " +
|
||||
"You MAY include [EXPR:n] to set your expression (n=1-6).";
|
||||
}
|
||||
|
||||
return $"{fullInstruction}\n{goodwillContext}\nIMPORTANT: Output XML tool calls only (or <no_action/>). " +
|
||||
return $"{fullInstruction}\n{goodwillContext}\nIMPORTANT: Output JSON tool calls only (or {\"tool_calls\": []}). " +
|
||||
$"You will produce the natural-language reply later and MUST use: {language}.";
|
||||
}
|
||||
|
||||
@@ -707,7 +706,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
"- modify_goodwill\n" +
|
||||
"- call_prefab_airdrop\n" +
|
||||
"- set_overwatch_mode\n" +
|
||||
"If no action is required, output exactly: <no_action/>.\n" +
|
||||
"If no action is required, output exactly: { \"tool_calls\": [] }.\n" +
|
||||
"Query tools exist but are disabled in this phase (not listed here).\n"
|
||||
: string.Empty;
|
||||
|
||||
@@ -716,14 +715,14 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
phaseInstruction += "\n- NATIVE MULTIMODAL: A current screenshot of the game is attached to this request. You can see the game state directly. Use it to determine coordinates for visual tools or to understand the context.";
|
||||
if (phase == RequestPhase.ActionTools)
|
||||
{
|
||||
phaseInstruction += "\n- VISUAL PHASE RULE: This phase is for ACTIONS only. If you want to describe the screen to the user, wait for the next phase (Reply Phase). Output XML actions only here.";
|
||||
phaseInstruction += "\n- VISUAL PHASE RULE: This phase is for ACTIONS only. If you want to describe the screen to the user, wait for the next phase (Reply Phase). Output JSON tool calls only here.";
|
||||
}
|
||||
}
|
||||
|
||||
string actionWhitelist = phase == RequestPhase.ActionTools
|
||||
? "ACTION PHASE VALID TAGS ONLY:\n" +
|
||||
"<spawn_resources>, <send_reinforcement>, <call_bombardment>, <modify_goodwill>, <call_prefab_airdrop>, <set_overwatch_mode>, <remember_fact>, <no_action/>\n" +
|
||||
"INVALID EXAMPLES (do NOT use now): <get_map_resources/>, <analyze_screen/>, <search_thing_def/>, <search_pawn_kind/>, <recall_memories/>\n"
|
||||
? "ACTION PHASE VALID TOOLS ONLY:\n" +
|
||||
"spawn_resources, send_reinforcement, call_bombardment, modify_goodwill, call_prefab_airdrop, set_overwatch_mode, remember_fact\n" +
|
||||
"INVALID EXAMPLES (do NOT use now): get_map_resources, analyze_screen, search_thing_def, search_pawn_kind, recall_memories\n"
|
||||
: string.Empty;
|
||||
|
||||
return string.Join("\n\n", new[]
|
||||
@@ -755,7 +754,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
sb.AppendLine("====");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("# TOOLS (AVAILABLE)");
|
||||
sb.AppendLine("Use XML tool calls only, or <no_action/> if no tools are needed.");
|
||||
sb.AppendLine("Use JSON tool calls only, or {\"tool_calls\": []} if no tools are needed.");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var tool in available)
|
||||
@@ -784,62 +783,58 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
"Goal: Gather info needed for decisions.\n" +
|
||||
"Rules:\n" +
|
||||
"- You MUST NOT write any natural language to the user in this phase.\n" +
|
||||
"- Output XML tool calls only, or exactly: <no_action/>.\n" +
|
||||
"- Output JSON tool calls only, or exactly: {\"tool_calls\": []}.\n" +
|
||||
"- Prefer query tools (get_*/search_*).\n" +
|
||||
"- CRITICAL: If the user asks for an ITEM (e.g. 'Reviver Mech Serum'), you MUST use <search_thing_def><query>...</query></search_thing_def> to find its exact DefName. NEVER GUESS DefNames.\n" +
|
||||
"- CRITICAL: If the user asks for an ITEM (e.g. 'Reviver Mech Serum'), you MUST use search_thing_def with {\"query\":\"...\"} to find its exact DefName. NEVER GUESS DefNames.\n" +
|
||||
"- You MAY call multiple tools in one response, but keep it concise.\n" +
|
||||
"- If the user requests multiple items or information, you MUST output ALL required tool calls in this SAME response.\n" +
|
||||
"- Action tools are available in PHASE 2 only; do NOT use them here.\n" +
|
||||
"After this phase, the game will automatically proceed to PHASE 2.\n" +
|
||||
"Output: XML only.\n",
|
||||
"Output: JSON only.\n",
|
||||
RequestPhase.ActionTools =>
|
||||
"# PHASE 2/3 (Action Tools)\n" +
|
||||
"Goal: Execute in-game actions based on known info.\n" +
|
||||
"Rules:\n" +
|
||||
"- You MUST NOT write any natural language to the user in this phase.\n" +
|
||||
"- Output XML tool calls only, or exactly: <no_action/>.\n" +
|
||||
"- Output JSON tool calls only, or exactly: {\"tool_calls\": []}.\n" +
|
||||
"- ONLY action tools are accepted in this phase (spawn_resources, send_reinforcement, call_bombardment, modify_goodwill, call_prefab_airdrop).\n" +
|
||||
"- Query tools (get_*/search_*) will be ignored.\n" +
|
||||
"- Prefer action tools (spawn_resources, send_reinforcement, call_bombardment, modify_goodwill).\n" +
|
||||
"- Avoid queries unless absolutely required.\n" +
|
||||
"- If no action is required based on query results, output <no_action/>.\n" +
|
||||
"- If you already executed the needed action earlier this turn, output <no_action/>.\n" +
|
||||
"- If no action is required based on query results, output {\"tool_calls\": []}.\n" +
|
||||
"- If you already executed the needed action earlier this turn, output {\"tool_calls\": []}.\n" +
|
||||
"After this phase, the game will automatically proceed to PHASE 3.\n" +
|
||||
"Output: XML only.\n",
|
||||
"Output: JSON only.\n",
|
||||
RequestPhase.Reply =>
|
||||
"# PHASE 3/3 (Reply)\n" +
|
||||
"Goal: Reply to the player.\n" +
|
||||
"Rules:\n" +
|
||||
"- Tool calls are DISABLED.\n" +
|
||||
"- You MUST write natural language only.\n" +
|
||||
"- Do NOT output any XML.\n" +
|
||||
"- Do NOT output any tool call JSON.\n" +
|
||||
"- If you want to set your expression, include: [EXPR:n] (n=1-6).\n",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsXmlToolCall(string response)
|
||||
private static bool IsToolCallJson(string response)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(response)) return false;
|
||||
return Regex.IsMatch(response, @"<(?!/?(i|b|color|size|material)\b)([a-zA-Z0-9_]+)(?:>.*?</\2>|/>)", RegexOptions.Singleline);
|
||||
return JsonToolCallParser.TryParseToolCallsFromText(response, out _, out _);
|
||||
}
|
||||
|
||||
private static bool IsNoActionOnly(string response)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(response)) return false;
|
||||
var matches = Regex.Matches(response, @"<([a-zA-Z0-9_]+)(?:>.*?</\1>|/>)", RegexOptions.Singleline);
|
||||
return matches.Count == 1 &&
|
||||
matches[0].Groups[1].Value.Equals("no_action", StringComparison.OrdinalIgnoreCase);
|
||||
if (!JsonToolCallParser.TryParseToolCallsFromText(response, out var toolCalls, out _)) return false;
|
||||
return toolCalls.Count == 0;
|
||||
}
|
||||
|
||||
private static bool HasActionToolCall(string response)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(response)) return false;
|
||||
var matches = Regex.Matches(response, @"<([a-zA-Z0-9_]+)(?:>.*?</\1>|/>)", RegexOptions.Singleline);
|
||||
foreach (Match match in matches)
|
||||
if (!JsonToolCallParser.TryParseToolCallsFromText(response, out var toolCalls, out _)) return false;
|
||||
foreach (var call in toolCalls)
|
||||
{
|
||||
var toolName = match.Groups[1].Value;
|
||||
if (IsActionToolName(toolName))
|
||||
if (IsActionToolName(call.Name))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -850,8 +845,15 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
private static bool ShouldRetryTools(string response)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(response)) return false;
|
||||
return Regex.IsMatch(response, @"<\s*retry_tools\s*/\s*>", RegexOptions.IgnoreCase) ||
|
||||
Regex.IsMatch(response, @"<\s*retry_tools\s*>", RegexOptions.IgnoreCase);
|
||||
if (!JsonToolCallParser.TryParseObject(response, out var obj)) return false;
|
||||
if (obj.TryGetValue("retry_tools", out object raw) && raw != null)
|
||||
{
|
||||
if (raw is bool b) return b;
|
||||
if (raw is string s && bool.TryParse(s, out bool parsed)) return parsed;
|
||||
if (raw is long l) return l != 0;
|
||||
if (raw is double d) return Math.Abs(d) > 0.0001;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int MaxToolsPerPhase(RequestPhase phase)
|
||||
@@ -981,7 +983,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
continue;
|
||||
}
|
||||
|
||||
// Revert UI filtering: Add assistant messages directly without stripping XML for history context
|
||||
// Revert UI filtering: Add assistant messages directly without stripping tool call JSON for history context
|
||||
filtered.Add(entry);
|
||||
}
|
||||
|
||||
@@ -1480,12 +1482,18 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
|
||||
}
|
||||
|
||||
private static string StripXmlTags(string text)
|
||||
private static string StripToolCallJson(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return text;
|
||||
string stripped = Regex.Replace(text, @"<(?!/?(i|b|color|size|material)\b)([a-zA-Z0-9_]+)[^>]*>.*?</\2>", "", RegexOptions.Singleline);
|
||||
stripped = Regex.Replace(stripped, @"<([a-zA-Z0-9_]+)[^>]*/>", "");
|
||||
return stripped;
|
||||
if (!JsonToolCallParser.TryParseToolCallsFromText(text, out _, out string fragment))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
int index = text.IndexOf(fragment, StringComparison.Ordinal);
|
||||
if (index < 0) return text;
|
||||
string cleaned = text.Remove(index, fragment.Length);
|
||||
return cleaned.Trim();
|
||||
}
|
||||
|
||||
private string StripExpressionTags(string text)
|
||||
@@ -1570,7 +1578,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
var client = new SimpleAIClient(apiKey, baseUrl, model, settings.useGeminiProtocol);
|
||||
_currentClient = client;
|
||||
|
||||
// Model-Driven Vision: Start with null image. The model must ask for it using <analyze_screen/> or <capture_screen/> if needed.
|
||||
// Model-Driven Vision: Start with null image. The model must request it using analyze_screen or capture_screen if needed.
|
||||
string base64Image = null;
|
||||
|
||||
|
||||
@@ -1588,16 +1596,16 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsXmlToolCall(queryResponse))
|
||||
if (!IsToolCallJson(queryResponse))
|
||||
{
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
WulaLog.Debug("[WulaAI] Turn 1/3 missing XML; treating as <no_action/>");
|
||||
WulaLog.Debug("[WulaAI] Turn 1/3 missing JSON tool calls; treating as no_action.");
|
||||
}
|
||||
queryResponse = "<no_action/>";
|
||||
queryResponse = "{\"tool_calls\": []}";
|
||||
}
|
||||
|
||||
PhaseExecutionResult queryResult = await ExecuteXmlToolsForPhase(queryResponse, queryPhase);
|
||||
PhaseExecutionResult queryResult = await ExecuteJsonToolsForPhase(queryResponse, queryPhase);
|
||||
|
||||
// DATA FLOW: If Query Phase captured an image, propagate it to subsequent phases.
|
||||
if (!string.IsNullOrEmpty(queryResult.CapturedImage))
|
||||
@@ -1613,9 +1621,9 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
string retryInstruction = persona +
|
||||
"\n\n# RETRY DECISION\n" +
|
||||
"No successful tool calls occurred in PHASE 1 (Query).\n" +
|
||||
"If you need to use tools in PHASE 1, output exactly: <retry_tools/>.\n" +
|
||||
"If you will proceed without actions, output exactly: <no_retry/>.\n" +
|
||||
"Output the XML tag only and NOTHING else.\n" +
|
||||
"If you need to use tools in PHASE 1, output exactly: {\"retry_tools\": true}.\n" +
|
||||
"If you will proceed without actions, output exactly: {\"retry_tools\": false}.\n" +
|
||||
"Output JSON only and NOTHING else.\n" +
|
||||
"\nLast user request:\n" + lastUserMessage;
|
||||
|
||||
string retryDecision = await client.GetChatCompletionAsync(retryInstruction, new List<(string role, string message)>(), maxTokens: 256, temperature: 0.1f);
|
||||
@@ -1628,7 +1636,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
|
||||
SetThinkingPhase(1, true);
|
||||
string retryQueryInstruction = GetToolSystemInstruction(queryPhase, !string.IsNullOrEmpty(base64Image)) +
|
||||
"\n\n# RETRY\nYou chose to retry. Output XML tool calls only (or <no_action/>).";
|
||||
"\n\n# RETRY\nYou chose to retry. Output JSON tool calls only (or {\"tool_calls\": []}).";
|
||||
string retryQueryResponse = await client.GetChatCompletionAsync(retryQueryInstruction, BuildToolContext(queryPhase), maxTokens: 2048, temperature: 0.1f, base64Image: base64Image);
|
||||
if (string.IsNullOrEmpty(retryQueryResponse))
|
||||
{
|
||||
@@ -1636,15 +1644,15 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsXmlToolCall(retryQueryResponse))
|
||||
if (!IsToolCallJson(retryQueryResponse))
|
||||
{
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
WulaLog.Debug("[WulaAI] Retry query phase missing XML; treating as <no_action/>");
|
||||
WulaLog.Debug("[WulaAI] Retry query phase missing JSON tool calls; treating as no_action.");
|
||||
}
|
||||
retryQueryResponse = "<no_action/>";
|
||||
retryQueryResponse = "{\"tool_calls\": []}";
|
||||
}
|
||||
queryResult = await ExecuteXmlToolsForPhase(retryQueryResponse, queryPhase);
|
||||
queryResult = await ExecuteJsonToolsForPhase(retryQueryResponse, queryPhase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1665,33 +1673,28 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
return;
|
||||
}
|
||||
|
||||
bool actionHasXml = IsXmlToolCall(actionResponse);
|
||||
bool actionIsNoActionOnly = IsNoActionOnly(actionResponse);
|
||||
bool actionHasActionTool = actionHasXml && HasActionToolCall(actionResponse);
|
||||
if (!actionHasXml || (!actionHasActionTool && !actionIsNoActionOnly))
|
||||
bool actionHasJson = IsToolCallJson(actionResponse);
|
||||
bool actionIsNoActionOnly = actionHasJson && IsNoActionOnly(actionResponse);
|
||||
bool actionHasActionTool = actionHasJson && HasActionToolCall(actionResponse);
|
||||
if (!actionHasJson || (!actionHasActionTool && !actionIsNoActionOnly))
|
||||
{
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
WulaLog.Debug("[WulaAI] Turn 2/3 missing XML or no action tool; attempting XML-only conversion.");
|
||||
WulaLog.Debug("[WulaAI] Turn 2/3 missing JSON or no action tool; attempting JSON-only conversion.");
|
||||
}
|
||||
string fixInstruction = "# FORMAT FIX (ACTION XML ONLY)\n" +
|
||||
string fixInstruction = "# FORMAT FIX (ACTION JSON ONLY)\n" +
|
||||
"Preserve the intent of the previous output.\n" +
|
||||
"If the previous output indicates no action is needed or refuses action, output exactly: <no_action/>.\n" +
|
||||
"If the previous output indicates no action is needed or refuses action, output exactly: {\"tool_calls\": []}.\n" +
|
||||
"Do NOT invent new actions.\n" +
|
||||
"Output VALID XML tool calls only. No natural language, no commentary.\nIgnore any non-XML text.\n" +
|
||||
"Allowed tags: <spawn_resources>, <send_reinforcement>, <call_bombardment>, <modify_goodwill>, <call_prefab_airdrop>, <no_action/>.\n" +
|
||||
"\nAction tool XML formats:\n" +
|
||||
"- <spawn_resources><items><item><name>DefName</name><count>Int</count></item></items></spawn_resources>\n" +
|
||||
"- <send_reinforcement><units>PawnKindDef: Count, ...</units></send_reinforcement>\n" +
|
||||
"- <call_bombardment><abilityDef>DefName</abilityDef><x>Int</x><z>Int</z></call_bombardment>\n" +
|
||||
"- <modify_goodwill><amount>Int</amount></modify_goodwill>\n" +
|
||||
"- <call_prefab_airdrop><prefabDefName>DefName</prefabDefName><x>Int</x><z>Int</z></call_prefab_airdrop>\n" +
|
||||
"Output VALID JSON tool calls only. No natural language, no commentary.\nIgnore any non-JSON text.\n" +
|
||||
"Allowed tools: spawn_resources, send_reinforcement, call_bombardment, modify_goodwill, call_prefab_airdrop, set_overwatch_mode, remember_fact.\n" +
|
||||
"Schema: {\"tool_calls\":[{\"type\":\"function\",\"function\":{\"name\":\"tool_name\",\"arguments\":{...}}}]}\n" +
|
||||
"\nPrevious output:\n" + TrimForPrompt(actionResponse, 600);
|
||||
string fixedResponse = await client.GetChatCompletionAsync(fixInstruction, actionContext, maxTokens: 2048, temperature: 0.1f);
|
||||
bool fixedHasXml = !string.IsNullOrEmpty(fixedResponse) && IsXmlToolCall(fixedResponse);
|
||||
bool fixedIsNoActionOnly = fixedHasXml && IsNoActionOnly(fixedResponse);
|
||||
bool fixedHasActionTool = fixedHasXml && HasActionToolCall(fixedResponse);
|
||||
if (fixedHasXml && (fixedHasActionTool || fixedIsNoActionOnly))
|
||||
bool fixedHasJson = !string.IsNullOrEmpty(fixedResponse) && IsToolCallJson(fixedResponse);
|
||||
bool fixedIsNoActionOnly = fixedHasJson && IsNoActionOnly(fixedResponse);
|
||||
bool fixedHasActionTool = fixedHasJson && HasActionToolCall(fixedResponse);
|
||||
if (fixedHasJson && (fixedHasActionTool || fixedIsNoActionOnly))
|
||||
{
|
||||
actionResponse = fixedResponse;
|
||||
}
|
||||
@@ -1699,12 +1702,12 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
{
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
WulaLog.Debug("[WulaAI] Turn 2/3 conversion failed; treating as <no_action/>");
|
||||
WulaLog.Debug("[WulaAI] Turn 2/3 conversion failed; treating as no_action.");
|
||||
}
|
||||
actionResponse = "<no_action/>";
|
||||
actionResponse = "{\"tool_calls\": []}";
|
||||
}
|
||||
}
|
||||
PhaseExecutionResult actionResult = await ExecuteXmlToolsForPhase(actionResponse, actionPhase);
|
||||
PhaseExecutionResult actionResult = await ExecuteJsonToolsForPhase(actionResponse, actionPhase);
|
||||
if (!actionResult.AnyActionSuccess && !_actionRetryUsed)
|
||||
{
|
||||
_actionRetryUsed = true;
|
||||
@@ -1713,9 +1716,9 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
string retryInstruction = persona +
|
||||
"\n\n# RETRY DECISION\n" +
|
||||
"No successful action tools occurred in PHASE 2 (Action).\n" +
|
||||
"If you need to execute an in-game action, output exactly: <retry_tools/>.\n" +
|
||||
"If you will proceed without actions, output exactly: <no_retry/>.\n" +
|
||||
"Output the XML tag only and NOTHING else.\n" +
|
||||
"If you need to execute an in-game action, output exactly: {\"retry_tools\": true}.\n" +
|
||||
"If you will proceed without actions, output exactly: {\"retry_tools\": false}.\n" +
|
||||
"Output JSON only and NOTHING else.\n" +
|
||||
"\nLast user request:\n" + lastUserMessage;
|
||||
|
||||
string retryDecision = await client.GetChatCompletionAsync(retryInstruction, new List<(string role, string message)>(), maxTokens: 256, temperature: 0.1f);
|
||||
@@ -1728,7 +1731,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
|
||||
SetThinkingPhase(2, true);
|
||||
string retryActionInstruction = GetToolSystemInstruction(actionPhase, !string.IsNullOrEmpty(base64Image)) +
|
||||
"\n\n# RETRY\nYou chose to retry. Output XML tool calls only (or <no_action/>).";
|
||||
"\n\n# RETRY\nYou chose to retry. Output JSON tool calls only (or {\"tool_calls\": []}).";
|
||||
var retryActionContext = BuildToolContext(actionPhase, includeUser: true);
|
||||
string retryActionResponse = await client.GetChatCompletionAsync(retryActionInstruction, retryActionContext, maxTokens: 2048, temperature: 0.1f, base64Image: base64Image);
|
||||
if (string.IsNullOrEmpty(retryActionResponse))
|
||||
@@ -1737,30 +1740,25 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsXmlToolCall(retryActionResponse))
|
||||
if (!IsToolCallJson(retryActionResponse))
|
||||
{
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
WulaLog.Debug("[WulaAI] Retry action phase missing XML; attempting XML-only conversion.");
|
||||
WulaLog.Debug("[WulaAI] Retry action phase missing JSON; attempting JSON-only conversion.");
|
||||
}
|
||||
string retryFixInstruction = "# FORMAT FIX (ACTION XML ONLY)\n" +
|
||||
string retryFixInstruction = "# FORMAT FIX (ACTION JSON ONLY)\n" +
|
||||
"Preserve the intent of the previous output.\n" +
|
||||
"If the previous output indicates no action is needed or refuses action, output exactly: <no_action/>.\n" +
|
||||
"If the previous output indicates no action is needed or refuses action, output exactly: {\"tool_calls\": []}.\n" +
|
||||
"Do NOT invent new actions.\n" +
|
||||
"Output VALID XML tool calls only. No natural language, no commentary.\nIgnore any non-XML text.\n" +
|
||||
"Allowed tags: <spawn_resources>, <send_reinforcement>, <call_bombardment>, <modify_goodwill>, <call_prefab_airdrop>, <no_action/>.\n" +
|
||||
"\nAction tool XML formats:\n" +
|
||||
"- <spawn_resources><items><item><name>DefName</name><count>Int</count></item></items></spawn_resources>\n" +
|
||||
"- <send_reinforcement><units>PawnKindDef: Count, ...</units></send_reinforcement>\n" +
|
||||
"- <call_bombardment><abilityDef>DefName</abilityDef><x>Int</x><z>Int</z></call_bombardment>\n" +
|
||||
"- <modify_goodwill><amount>Int</amount></modify_goodwill>\n" +
|
||||
"- <call_prefab_airdrop><prefabDefName>DefName</prefabDefName><x>Int</x><z>Int</z></call_prefab_airdrop>\n" +
|
||||
"Output VALID JSON tool calls only. No natural language, no commentary.\nIgnore any non-JSON text.\n" +
|
||||
"Allowed tools: spawn_resources, send_reinforcement, call_bombardment, modify_goodwill, call_prefab_airdrop, set_overwatch_mode, remember_fact.\n" +
|
||||
"Schema: {\"tool_calls\":[{\"type\":\"function\",\"function\":{\"name\":\"tool_name\",\"arguments\":{...}}}]}\n" +
|
||||
"\nPrevious output:\n" + TrimForPrompt(retryActionResponse, 600);
|
||||
string retryFixedResponse = await client.GetChatCompletionAsync(retryFixInstruction, retryActionContext, maxTokens: 2048, temperature: 0.1f);
|
||||
bool retryFixedHasXml = !string.IsNullOrEmpty(retryFixedResponse) && IsXmlToolCall(retryFixedResponse);
|
||||
bool retryFixedIsNoActionOnly = retryFixedHasXml && IsNoActionOnly(retryFixedResponse);
|
||||
bool retryFixedHasActionTool = retryFixedHasXml && HasActionToolCall(retryFixedResponse);
|
||||
if (retryFixedHasXml && (retryFixedHasActionTool || retryFixedIsNoActionOnly))
|
||||
bool retryFixedHasJson = !string.IsNullOrEmpty(retryFixedResponse) && IsToolCallJson(retryFixedResponse);
|
||||
bool retryFixedIsNoActionOnly = retryFixedHasJson && IsNoActionOnly(retryFixedResponse);
|
||||
bool retryFixedHasActionTool = retryFixedHasJson && HasActionToolCall(retryFixedResponse);
|
||||
if (retryFixedHasJson && (retryFixedHasActionTool || retryFixedIsNoActionOnly))
|
||||
{
|
||||
retryActionResponse = retryFixedResponse;
|
||||
}
|
||||
@@ -1768,13 +1766,13 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
{
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
WulaLog.Debug("[WulaAI] Retry action conversion failed; treating as <no_action/>");
|
||||
WulaLog.Debug("[WulaAI] Retry action conversion failed; treating as no_action.");
|
||||
}
|
||||
retryActionResponse = "<no_action/>";
|
||||
retryActionResponse = "{\"tool_calls\": []}";
|
||||
}
|
||||
}
|
||||
|
||||
actionResult = await ExecuteXmlToolsForPhase(retryActionResponse, actionPhase);
|
||||
actionResult = await ExecuteJsonToolsForPhase(retryActionResponse, actionPhase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1826,29 +1824,29 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
return;
|
||||
}
|
||||
|
||||
bool replyHadXml = IsXmlToolCall(reply);
|
||||
string strippedReply = StripXmlTags(reply)?.Trim() ?? "";
|
||||
if (replyHadXml || string.IsNullOrWhiteSpace(strippedReply))
|
||||
bool replyHadToolCalls = IsToolCallJson(reply);
|
||||
string strippedReply = StripToolCallJson(reply)?.Trim() ?? "";
|
||||
if (replyHadToolCalls || string.IsNullOrWhiteSpace(strippedReply))
|
||||
{
|
||||
string retryReplyInstruction = replyInstruction +
|
||||
"\n\n# RETRY (REPLY OUTPUT)\n" +
|
||||
"Your last reply included XML or was empty. Tool calls are DISABLED.\n" +
|
||||
"You MUST reply in natural language only. Do NOT output any XML.\n";
|
||||
"Your last reply included tool call JSON or was empty. Tool calls are DISABLED.\n" +
|
||||
"You MUST reply in natural language only. Do NOT output any tool call JSON.\n";
|
||||
string retryReply = await client.GetChatCompletionAsync(retryReplyInstruction, BuildReplyHistory(), maxTokens: 256, temperature: 0.3f);
|
||||
if (!string.IsNullOrEmpty(retryReply))
|
||||
{
|
||||
reply = retryReply;
|
||||
replyHadXml = IsXmlToolCall(reply);
|
||||
strippedReply = StripXmlTags(reply)?.Trim() ?? "";
|
||||
replyHadToolCalls = IsToolCallJson(reply);
|
||||
strippedReply = StripToolCallJson(reply)?.Trim() ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
if (replyHadXml)
|
||||
if (replyHadToolCalls)
|
||||
{
|
||||
string cleaned = StripXmlTags(reply)?.Trim() ?? "";
|
||||
string cleaned = StripToolCallJson(reply)?.Trim() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(cleaned))
|
||||
{
|
||||
cleaned = "(system) AI reply returned tool XML only and was discarded. Please retry or send /clear to reset context.";
|
||||
cleaned = "(system) AI reply returned tool call JSON only and was discarded. Please retry or send /clear to reset context.";
|
||||
}
|
||||
reply = cleaned;
|
||||
}
|
||||
@@ -1866,7 +1864,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
SetThinkingState(false);
|
||||
}
|
||||
}
|
||||
private async Task<PhaseExecutionResult> ExecuteXmlToolsForPhase(string xml, RequestPhase phase)
|
||||
private async Task<PhaseExecutionResult> ExecuteJsonToolsForPhase(string json, RequestPhase phase)
|
||||
{
|
||||
if (phase == RequestPhase.Reply)
|
||||
{
|
||||
@@ -1874,14 +1872,23 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
return default;
|
||||
}
|
||||
|
||||
string guidance = "ToolRunner Guidance: Reply to the player in natural language only. Do NOT output any XML. You may include [EXPR:n] to set expression (n=1-6).";
|
||||
string guidance = "ToolRunner Guidance: Reply to the player in natural language only. Do NOT output any tool call JSON. You may include [EXPR:n] to set expression (n=1-6).";
|
||||
|
||||
var matches = Regex.Matches(xml ?? "", @"<([a-zA-Z0-9_]+)(?:>.*?</\1>|/>)", RegexOptions.Singleline);
|
||||
|
||||
if (matches.Count == 0 || (matches.Count == 1 && matches[0].Groups[1].Value.Equals("no_action", StringComparison.OrdinalIgnoreCase)))
|
||||
if (!JsonToolCallParser.TryParseToolCallsFromText(json ?? "", out var toolCalls, out string jsonFragment))
|
||||
{
|
||||
UpdatePhaseToolLedger(phase, false, new List<string>());
|
||||
_history.Add(("toolcall", "<no_action/>"));
|
||||
_history.Add(("toolcall", "{\"tool_calls\": []}"));
|
||||
_history.Add(("tool", $"[Tool Results]\nTool 'no_action' Result: No action taken.\n{guidance}"));
|
||||
PersistHistory();
|
||||
UpdateActionLedgerNote();
|
||||
await Task.CompletedTask;
|
||||
return default;
|
||||
}
|
||||
|
||||
if (toolCalls.Count == 0)
|
||||
{
|
||||
UpdatePhaseToolLedger(phase, false, new List<string>());
|
||||
_history.Add(("toolcall", "{\"tool_calls\": []}"));
|
||||
_history.Add(("tool", $"[Tool Results]\nTool 'no_action' Result: No action taken.\n{guidance}"));
|
||||
PersistHistory();
|
||||
UpdateActionLedgerNote();
|
||||
@@ -1897,43 +1904,58 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
var successfulActions = new List<string>();
|
||||
var failedActions = new List<string>();
|
||||
var nonActionToolsInActionPhase = new List<string>();
|
||||
var historyCalls = new List<Dictionary<string, object>>();
|
||||
StringBuilder combinedResults = new StringBuilder();
|
||||
StringBuilder xmlOnlyBuilder = new StringBuilder();
|
||||
string capturedImageForPhase = null;
|
||||
|
||||
bool countActionSuccessOnly = phase == RequestPhase.ActionTools;
|
||||
|
||||
foreach (Match match in matches)
|
||||
foreach (var call in toolCalls)
|
||||
{
|
||||
if (executed >= maxTools)
|
||||
{
|
||||
combinedResults.AppendLine($"ToolRunner Note: Skipped remaining tools because this phase allows at most {maxTools} tool call(s)." );
|
||||
combinedResults.AppendLine($"ToolRunner Note: Skipped remaining tools because this phase allows at most {maxTools} tool call(s).");
|
||||
break;
|
||||
}
|
||||
|
||||
string toolCallXml = match.Value;
|
||||
string toolName = match.Groups[1].Value;
|
||||
|
||||
if (toolName.Equals("no_action", StringComparison.OrdinalIgnoreCase))
|
||||
string toolName = call.Name;
|
||||
if (string.IsNullOrWhiteSpace(toolName))
|
||||
{
|
||||
combinedResults.AppendLine("ToolRunner Note: Ignored <no_action/> because other tool calls were present.");
|
||||
executed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (toolName.Equals("analyze_screen", StringComparison.OrdinalIgnoreCase) || toolName.Equals("capture_screen", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(toolName, "no_action", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Intercept Vision Request: Capture screen and return it.
|
||||
// We skip the tool's internal execution to save time/tokens, as the purpose is just to get the image into the context.
|
||||
capturedImageForPhase = ScreenCaptureUtility.CaptureScreenAsBase64();
|
||||
combinedResults.AppendLine($"Tool '{toolName}' Result: Screen captured successfully. Context updated for next phase.");
|
||||
successfulToolCall = true;
|
||||
successfulTools.Add(toolName);
|
||||
executed++;
|
||||
continue;
|
||||
combinedResults.AppendLine("ToolRunner Note: Ignored 'no_action' tool because other tool calls were present.");
|
||||
executed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (xmlOnlyBuilder.Length > 0) xmlOnlyBuilder.AppendLine().AppendLine();
|
||||
xmlOnlyBuilder.Append(toolCallXml);
|
||||
var historyCall = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "function",
|
||||
["function"] = new Dictionary<string, object>
|
||||
{
|
||||
["name"] = toolName,
|
||||
["arguments"] = call.Arguments ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||
}
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(call.Id))
|
||||
{
|
||||
historyCall["id"] = call.Id;
|
||||
}
|
||||
historyCalls.Add(historyCall);
|
||||
|
||||
if (toolName.Equals("analyze_screen", StringComparison.OrdinalIgnoreCase) || toolName.Equals("capture_screen", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
capturedImageForPhase = ScreenCaptureUtility.CaptureScreenAsBase64();
|
||||
combinedResults.AppendLine($"Tool '{toolName}' Result: Screen captured successfully. Context updated for next phase.");
|
||||
successfulToolCall = true;
|
||||
successfulTools.Add(toolName);
|
||||
executed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (phase == RequestPhase.ActionTools && IsQueryToolName(toolName))
|
||||
{
|
||||
@@ -1952,19 +1974,13 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
continue;
|
||||
}
|
||||
|
||||
string argsXml = toolCallXml;
|
||||
var contentMatch = Regex.Match(toolCallXml, $@"<{toolName}>(.*?)</{toolName}>", RegexOptions.Singleline);
|
||||
if (contentMatch.Success)
|
||||
{
|
||||
argsXml = contentMatch.Groups[1].Value;
|
||||
}
|
||||
|
||||
string argsJson = call.ArgumentsJson ?? "{}";
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
WulaLog.Debug($"[WulaAI] Executing tool (phase {phase}): {toolName} with args: {argsXml}");
|
||||
WulaLog.Debug($"[WulaAI] Executing tool (phase {phase}): {toolName} with args: {argsJson}");
|
||||
}
|
||||
|
||||
string result = (await tool.ExecuteAsync(argsXml)).Trim();
|
||||
string result = (await tool.ExecuteAsync(argsJson)).Trim();
|
||||
bool isError = !string.IsNullOrEmpty(result) && result.StartsWith("Error:", StringComparison.OrdinalIgnoreCase);
|
||||
if (toolName == "modify_goodwill")
|
||||
{
|
||||
@@ -2009,10 +2025,9 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
executed++;
|
||||
}
|
||||
|
||||
string nonXmlText = StripXmlTags(xml);
|
||||
if (!string.IsNullOrWhiteSpace(nonXmlText))
|
||||
if (!string.IsNullOrWhiteSpace(jsonFragment) && !string.Equals((json ?? "").Trim(), jsonFragment, StringComparison.Ordinal))
|
||||
{
|
||||
combinedResults.AppendLine("ToolRunner Note: Non-XML text in the tool phase was ignored.");
|
||||
combinedResults.AppendLine("ToolRunner Note: Non-JSON text in the tool phase was ignored.");
|
||||
}
|
||||
if (phase == RequestPhase.ActionTools && nonActionToolsInActionPhase.Count > 0)
|
||||
{
|
||||
@@ -2032,8 +2047,10 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
}
|
||||
combinedResults.AppendLine(guidance);
|
||||
|
||||
string xmlOnly = xmlOnlyBuilder.Length == 0 ? "<no_action/>" : xmlOnlyBuilder.ToString().Trim();
|
||||
_history.Add(("toolcall", xmlOnly));
|
||||
string toolCallsJson = historyCalls.Count == 0
|
||||
? "{\"tool_calls\": []}"
|
||||
: JsonToolCallParser.SerializeToJson(new Dictionary<string, object> { ["tool_calls"] = historyCalls });
|
||||
_history.Add(("toolcall", toolCallsJson));
|
||||
_history.Add(("tool", $"[Tool Results]\n{combinedResults.ToString().Trim()}"));
|
||||
PersistHistory();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user