diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll
index b70ccc57..6af81769 100644
Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/1.6/Assemblies/WulaFallenEmpire.dll differ
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
index 981789c4..dca3b0da 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
@@ -98,6 +98,24 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
- 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, output { ""tool_calls"": [] } and proceed to the next phase.";
+ private const string QwenToolRulesTemplate = @"
+# TOOLS
+You may call one or more functions to assist with the user query.
+
+You are provided with function signatures within XML tags:
+
+{tool_descs}
+
+
+For each function call, return a JSON object within tags:
+
+{""name"": ""tool_name"", ""arguments"": { ... }}
+
+
+- Output ONLY tool calls in Query/Action phases.
+- If no tools are needed, output exactly: {""tool_calls"": []}
+- Do NOT include natural language outside tool calls in tool phases.
+- Never invent tools, parameters, defNames, coordinates, or tool results.";
public AIIntelligenceCore(World world) : base(world)
{
Instance = this;
@@ -639,8 +657,10 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
{
string persona = GetActivePersona();
string personaBlock = persona;
- string phaseInstruction = GetPhaseInstruction(phase).TrimEnd();
- string toolsForThisPhase = BuildToolsForPhase(phase);
+ var settings = WulaFallenEmpireMod.settings;
+ bool useQwenTemplate = settings != null && !settings.useNativeToolApi;
+ string phaseInstruction = GetPhaseInstruction(phase, useQwenTemplate).TrimEnd();
+ string toolsForThisPhase = useQwenTemplate ? BuildQwenToolsForPhase(phase) : BuildToolsForPhase(phase);
string actionPriority = phase == RequestPhase.ActionTools
? "ACTION TOOL PRIORITY:\n" +
"- spawn_resources\n" +
@@ -665,6 +685,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
"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;
+ string toolRules = useQwenTemplate ? null : ToolRulesInstruction.TrimEnd();
return string.Join("\n\n", new[]
{
personaBlock,
@@ -672,13 +693,14 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
"IMPORTANT: Long-term memory is not preloaded. Use recall_memories to fetch memories when needed.",
string.IsNullOrWhiteSpace(actionPriority) ? null : actionPriority.TrimEnd(),
string.IsNullOrWhiteSpace(actionWhitelist) ? null : actionWhitelist.TrimEnd(),
- ToolRulesInstruction.TrimEnd(),
+ toolRules,
toolsForThisPhase
}.Where(part => !string.IsNullOrWhiteSpace(part)));
}
private string BuildToolsForPhase(RequestPhase phase)
{
if (phase == RequestPhase.Reply) return "";
+ var settings = WulaFallenEmpireMod.settings;
var available = _tools
.Where(t => t != null)
.Where(t => phase == RequestPhase.QueryTools
@@ -686,6 +708,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
: phase == RequestPhase.ActionTools
? IsActionToolName(t.Name)
: true)
+ .Where(t => !(IsVlmToolName(t.Name) && settings?.enableVlmFeatures != true))
.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
StringBuilder sb = new StringBuilder();
@@ -709,8 +732,35 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
}
return sb.ToString().TrimEnd();
}
+ private string BuildQwenToolsForPhase(RequestPhase phase)
+ {
+ if (phase == RequestPhase.Reply) return "";
+ var settings = WulaFallenEmpireMod.settings;
+ var available = _tools
+ .Where(t => t != null)
+ .Where(t => phase == RequestPhase.QueryTools
+ ? IsQueryToolName(t.Name)
+ : phase == RequestPhase.ActionTools
+ ? IsActionToolName(t.Name)
+ : true)
+ .Where(t => !(IsVlmToolName(t.Name) && settings?.enableVlmFeatures != true))
+ .OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ var toolLines = new List();
+ foreach (var tool in available)
+ {
+ var def = tool.GetFunctionDefinition();
+ if (def != null)
+ {
+ toolLines.Add(JsonToolCallParser.SerializeToJson(def));
+ }
+ }
+ string toolDescBlock = string.Join("\n", toolLines);
+ return QwenToolRulesTemplate.Replace("{tool_descs}", toolDescBlock).TrimEnd();
+ }
private List> BuildNativeToolDefinitions(RequestPhase phase)
{
+ var settings = WulaFallenEmpireMod.settings;
var available = _tools
.Where(t => t != null)
.Where(t => phase == RequestPhase.QueryTools
@@ -718,6 +768,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
: phase == RequestPhase.ActionTools
? IsActionToolName(t.Name)
: false)
+ .Where(t => !(IsVlmToolName(t.Name) && settings?.enableVlmFeatures != true))
.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
var definitions = new List>();
@@ -731,8 +782,13 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
}
return definitions;
}
- private static string GetPhaseInstruction(RequestPhase phase)
+ private static string GetPhaseInstruction(RequestPhase phase, bool useQwenTemplate)
{
+ string toolOutputRule = useQwenTemplate
+ ? "- Output tool calls only using {\"name\":\"tool_name\",\"arguments\":{...}}.\n" +
+ "- If no tools are needed, output exactly: {\"tool_calls\": []}.\n"
+ : "- Output JSON tool calls only, or exactly: {\"tool_calls\": []}.\n";
+ string outputFooter = useQwenTemplate ? "Output: tool calls only.\n" : "Output: JSON only.\n";
return phase switch
{
RequestPhase.QueryTools =>
@@ -740,20 +796,20 @@ 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 JSON tool calls only, or exactly: {\"tool_calls\": []}.\n" +
+ toolOutputRule +
"- 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 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: JSON only.\n",
+ outputFooter,
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 JSON tool calls only, or exactly: {\"tool_calls\": []}.\n" +
+ toolOutputRule +
"- 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" +
@@ -761,7 +817,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
"- 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: JSON only.\n",
+ outputFooter,
RequestPhase.Reply =>
"# PHASE 3/3 (Reply)\n" +
"Goal: Reply to the player.\n" +
@@ -845,7 +901,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
private bool IsToolAvailable(string toolName)
{
if (string.IsNullOrWhiteSpace(toolName)) return false;
- if (string.Equals(toolName, "capture_screen", StringComparison.OrdinalIgnoreCase))
+ if (IsVlmToolName(toolName))
{
return WulaFallenEmpireMod.settings?.enableVlmFeatures == true;
}
@@ -948,6 +1004,10 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
toolName.StartsWith("analyze_", StringComparison.OrdinalIgnoreCase) ||
toolName == "recall_memories";
}
+ private static bool IsVlmToolName(string toolName)
+ {
+ return toolName == "analyze_screen" || toolName == "capture_screen";
+ }
private static string SanitizeToolResultForActionPhase(string message)
{
if (string.IsNullOrWhiteSpace(message)) return message;
@@ -1763,7 +1823,8 @@ private List BuildNativeHistory()
string model = settings.useGeminiProtocol ? settings.geminiModel : settings.model;
var client = new SimpleAIClient(apiKey, baseUrl, model, settings.useGeminiProtocol);
_currentClient = client;
- if (!settings.useGeminiProtocol)
+ bool useQwenTemplate = !settings.useNativeToolApi;
+ if (settings.useNativeToolApi)
{
await RunNativeToolLoopAsync(client, settings);
return;
@@ -1955,7 +2016,7 @@ private List BuildNativeHistory()
WulaLog.Debug($"[WulaAI] ===== Turn 3/3 ({replyPhase}) =====");
}
SetThinkingPhase(3, false);
- string replyInstruction = GetSystemInstruction(false, "") + "\n\n" + GetPhaseInstruction(replyPhase);
+ string replyInstruction = GetSystemInstruction(false, "") + "\n\n" + GetPhaseInstruction(replyPhase, useQwenTemplate);
if (!string.IsNullOrWhiteSpace(_queryToolLedgerNote))
{
replyInstruction += "\n" + _queryToolLedgerNote;
@@ -2167,7 +2228,7 @@ private List BuildNativeHistory()
WulaLog.Debug("[WulaAI] ===== Turn 3/3 (Reply) =====");
}
SetThinkingPhase(3, false);
- string replyInstruction = GetSystemInstruction(false, "") + "\n\n" + GetPhaseInstruction(replyPhase);
+ string replyInstruction = GetSystemInstruction(false, "") + "\n\n" + GetPhaseInstruction(replyPhase, false);
if (!string.IsNullOrWhiteSpace(_queryToolLedgerNote))
{
replyInstruction += "\n" + _queryToolLedgerNote;
@@ -2315,9 +2376,18 @@ private List BuildNativeHistory()
executed++;
continue;
}
- if (string.Equals(call.Name, "analyze_screen", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(call.Name, "capture_screen", StringComparison.OrdinalIgnoreCase))
+ if (IsVlmToolName(call.Name))
{
+ if (phase != RequestPhase.QueryTools || WulaFallenEmpireMod.settings?.enableVlmFeatures != true)
+ {
+ string note = $"ToolRunner Note: Ignored visual tool in this phase: {call.Name}.";
+ combinedResults.AppendLine(note);
+ messages?.Add(ChatMessage.ToolResult(call.Id ?? "", note));
+ historyToolResults.Add((call.Id ?? "", note));
+ nonActionToolsInActionPhase.Add(call.Name);
+ executed++;
+ continue;
+ }
capturedImageForPhase = ScreenCaptureUtility.CaptureScreenAsBase64();
string resultText = "Screen captured successfully. Context updated for next phase.";
combinedResults.AppendLine($"Tool '{call.Name}' Result: {resultText}");
@@ -2534,8 +2604,16 @@ private async Task ExecuteJsonToolsForPhase(string json, R
historyCall["id"] = call.Id;
}
historyCalls.Add(historyCall);
- if (toolName.Equals("analyze_screen", StringComparison.OrdinalIgnoreCase) || toolName.Equals("capture_screen", StringComparison.OrdinalIgnoreCase))
+ if (IsVlmToolName(toolName))
{
+ if (phase != RequestPhase.QueryTools || WulaFallenEmpireMod.settings?.enableVlmFeatures != true)
+ {
+ combinedResults.AppendLine($"ToolRunner Note: Ignored visual tool in this phase: {toolName}.");
+ historyToolResults.Add((call.Id ?? "", $"ToolRunner Note: Ignored visual tool in this phase: {toolName}."));
+ nonActionToolsInActionPhase.Add(toolName);
+ executed++;
+ continue;
+ }
capturedImageForPhase = ScreenCaptureUtility.CaptureScreenAsBase64();
combinedResults.AppendLine($"Tool '{toolName}' Result: Screen captured successfully. Context updated for next phase.");
historyToolResults.Add((call.Id ?? "", $"Tool '{toolName}' Result: Screen captured successfully. Context updated for next phase."));
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Utils/JsonToolCallParser.cs b/Source/WulaFallenEmpire/EventSystem/AI/Utils/JsonToolCallParser.cs
index 41aca837..275abe51 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/Utils/JsonToolCallParser.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/Utils/JsonToolCallParser.cs
@@ -80,6 +80,11 @@ namespace WulaFallenEmpire.EventSystem.AI.Utils
return true;
}
+ if (TryParseToolCallsFromQwenText(trimmed, out toolCalls, out jsonFragment))
+ {
+ return true;
+ }
+
int firstBrace = trimmed.IndexOf('{');
int lastBrace = trimmed.LastIndexOf('}');
if (firstBrace >= 0 && lastBrace > firstBrace)
@@ -99,6 +104,79 @@ namespace WulaFallenEmpire.EventSystem.AI.Utils
return false;
}
+ private static bool TryParseToolCallsFromQwenText(string input, out List toolCalls, out string jsonFragment)
+ {
+ toolCalls = null;
+ jsonFragment = null;
+ if (string.IsNullOrWhiteSpace(input)) return false;
+
+ const string startTag = "";
+ const string endTag = "";
+ int index = 0;
+ var parsedCalls = new List();
+
+ while (index < input.Length)
+ {
+ int start = IndexOfIgnoreCase(input, startTag, index);
+ if (start < 0) break;
+ int end = IndexOfIgnoreCase(input, endTag, start + startTag.Length);
+ if (end < 0) break;
+
+ string inner = input.Substring(start + startTag.Length, end - (start + startTag.Length)).Trim();
+ if (TryParseObject(inner, out Dictionary obj))
+ {
+ string name = TryGetString(obj, "name");
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ object argsObj = null;
+ TryGetValue(obj, "arguments", out argsObj);
+ if (!TryNormalizeArguments(argsObj, out Dictionary args, out string argsJson))
+ {
+ args = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ argsJson = "{}";
+ }
+ string id = TryGetString(obj, "id");
+ parsedCalls.Add(new ToolCallInfo
+ {
+ Id = id,
+ Name = name.Trim(),
+ Arguments = args,
+ ArgumentsJson = argsJson
+ });
+ }
+ }
+
+ index = end + endTag.Length;
+ }
+
+ if (parsedCalls.Count == 0) return false;
+
+ toolCalls = parsedCalls;
+ var callEntries = new List