This commit is contained in:
2025-12-22 17:46:21 +08:00
parent 94c9cd9a84
commit d5c222fb41
2 changed files with 167 additions and 32 deletions

View File

@@ -270,13 +270,19 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
"- call_bombardment\n" + "- call_bombardment\n" +
"- modify_goodwill\n" + "- modify_goodwill\n" +
"If no action is required, output exactly: <no_action/>.\n" + "If no action is required, output exactly: <no_action/>.\n" +
"Other tools are still available if needed.\n" "Query tools exist but are disabled in this phase (not listed here).\n"
: string.Empty;
string actionWhitelist = phase == RequestPhase.ActionTools
? "ACTION PHASE VALID TAGS ONLY:\n" +
"<spawn_resources>, <send_reinforcement>, <call_bombardment>, <modify_goodwill>, <no_action/>\n" +
"INVALID EXAMPLES (do NOT use now): <get_map_resources/>, <search_thing_def/>\n"
: string.Empty; : string.Empty;
return string.Join("\n\n", new[] return string.Join("\n\n", new[]
{ {
phaseInstruction, phaseInstruction,
string.IsNullOrWhiteSpace(actionPriority) ? null : actionPriority.TrimEnd(), string.IsNullOrWhiteSpace(actionPriority) ? null : actionPriority.TrimEnd(),
string.IsNullOrWhiteSpace(actionWhitelist) ? null : actionWhitelist.TrimEnd(),
ToolRulesInstruction.TrimEnd(), ToolRulesInstruction.TrimEnd(),
toolsForThisPhase toolsForThisPhase
}.Where(part => !string.IsNullOrWhiteSpace(part))); }.Where(part => !string.IsNullOrWhiteSpace(part)));
@@ -288,6 +294,11 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
var available = _tools var available = _tools
.Where(t => t != null) .Where(t => t != null)
.Where(t => phase == RequestPhase.QueryTools
? IsQueryToolName(t.Name)
: phase == RequestPhase.ActionTools
? IsActionToolName(t.Name)
: true)
.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase) .OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
@@ -328,6 +339,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
"- Prefer query tools (get_*/search_*).\n" + "- Prefer query tools (get_*/search_*).\n" +
"- You MAY call multiple tools in one response, but keep it concise.\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" + "- 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" + "After this phase, the game will automatically proceed to PHASE 2.\n" +
"Output: XML only.\n", "Output: XML only.\n",
RequestPhase.ActionTools => RequestPhase.ActionTools =>
@@ -379,7 +391,38 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
}; };
} }
private List<(string role, string message)> BuildToolContext(int maxToolResults = 2) private static bool IsActionToolName(string toolName)
{
return toolName == "spawn_resources" ||
toolName == "send_reinforcement" ||
toolName == "call_bombardment" ||
toolName == "modify_goodwill";
}
private static bool IsQueryToolName(string toolName)
{
if (string.IsNullOrWhiteSpace(toolName)) return false;
return toolName.StartsWith("get_", StringComparison.OrdinalIgnoreCase) ||
toolName.StartsWith("search_", StringComparison.OrdinalIgnoreCase);
}
private static string SanitizeToolResultForActionPhase(string message)
{
if (string.IsNullOrWhiteSpace(message)) return message;
string sanitized = message;
sanitized = Regex.Replace(sanitized, @"Tool\s+'[^']+'\s+Result(?:\s+\(Invisible\))?:", "Query Result:");
sanitized = Regex.Replace(sanitized, @"Tool\s+'[^']+'\s+Result\s+\(Invisible\):", "Query Result:");
return sanitized;
}
private static string TrimForPrompt(string text, int maxChars)
{
if (string.IsNullOrWhiteSpace(text)) return "";
if (text.Length <= maxChars) return text;
return text.Substring(0, maxChars) + "...(truncated)";
}
private List<(string role, string message)> BuildToolContext(RequestPhase phase, int maxToolResults = 2, bool includeUser = true)
{ {
if (_history == null || _history.Count == 0) return new List<(string role, string message)>(); if (_history == null || _history.Count == 0) return new List<(string role, string message)>();
@@ -400,7 +443,12 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
{ {
if (string.Equals(_history[i].role, "tool", StringComparison.OrdinalIgnoreCase)) if (string.Equals(_history[i].role, "tool", StringComparison.OrdinalIgnoreCase))
{ {
toolEntries.Add(_history[i]); string msg = _history[i].message;
if (phase == RequestPhase.ActionTools)
{
msg = SanitizeToolResultForActionPhase(msg);
}
toolEntries.Add((_history[i].role, msg));
} }
} }
@@ -409,10 +457,12 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
toolEntries = toolEntries.Skip(toolEntries.Count - maxToolResults).ToList(); toolEntries = toolEntries.Skip(toolEntries.Count - maxToolResults).ToList();
} }
var context = new List<(string role, string message)> bool includeUserFallback = includeUser || toolEntries.Count == 0;
var context = new List<(string role, string message)>();
if (includeUserFallback)
{ {
_history[lastUserIndex] context.Add(_history[lastUserIndex]);
}; }
context.AddRange(toolEntries); context.AddRange(toolEntries);
return context; return context;
} }
@@ -459,7 +509,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
} }
string queryInstruction = GetToolSystemInstruction(queryPhase); string queryInstruction = GetToolSystemInstruction(queryPhase);
string queryResponse = await client.GetChatCompletionAsync(queryInstruction, BuildToolContext(), maxTokens: 128, temperature: 0.1f); string queryResponse = await client.GetChatCompletionAsync(queryInstruction, BuildToolContext(queryPhase), maxTokens: 128, temperature: 0.1f);
if (string.IsNullOrEmpty(queryResponse)) if (string.IsNullOrEmpty(queryResponse))
{ {
_currentResponse = "Wula_AI_Error_ConnectionLost".Translate(); _currentResponse = "Wula_AI_Error_ConnectionLost".Translate();
@@ -501,7 +551,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
SetThinkingPhase(1, true); SetThinkingPhase(1, true);
string retryQueryInstruction = GetToolSystemInstruction(queryPhase) + string retryQueryInstruction = GetToolSystemInstruction(queryPhase) +
"\n\n# RETRY\nYou chose to retry. Output XML tool calls only (or <no_action/>)."; "\n\n# RETRY\nYou chose to retry. Output XML tool calls only (or <no_action/>).";
string retryQueryResponse = await client.GetChatCompletionAsync(retryQueryInstruction, BuildToolContext(), maxTokens: 128, temperature: 0.1f); string retryQueryResponse = await client.GetChatCompletionAsync(retryQueryInstruction, BuildToolContext(queryPhase), maxTokens: 128, temperature: 0.1f);
if (string.IsNullOrEmpty(retryQueryResponse)) if (string.IsNullOrEmpty(retryQueryResponse))
{ {
_currentResponse = "Wula_AI_Error_ConnectionLost".Translate(); _currentResponse = "Wula_AI_Error_ConnectionLost".Translate();
@@ -529,7 +579,8 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
SetThinkingPhase(2, false); SetThinkingPhase(2, false);
string actionInstruction = GetToolSystemInstruction(actionPhase); string actionInstruction = GetToolSystemInstruction(actionPhase);
string actionResponse = await client.GetChatCompletionAsync(actionInstruction, BuildToolContext(), maxTokens: 128, temperature: 0.1f); var actionContext = BuildToolContext(actionPhase, includeUser: true);
string actionResponse = await client.GetChatCompletionAsync(actionInstruction, actionContext, maxTokens: 128, temperature: 0.1f);
if (string.IsNullOrEmpty(actionResponse)) if (string.IsNullOrEmpty(actionResponse))
{ {
_currentResponse = "Wula_AI_Error_ConnectionLost".Translate(); _currentResponse = "Wula_AI_Error_ConnectionLost".Translate();
@@ -540,9 +591,28 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
{ {
if (Prefs.DevMode) if (Prefs.DevMode)
{ {
WulaLog.Debug("[WulaAI] Turn 2/3 missing XML; treating as <no_action/>"); WulaLog.Debug("[WulaAI] Turn 2/3 missing XML; attempting XML-only conversion.");
}
string fixInstruction = actionInstruction +
"\n\n# FORMAT FIX\n" +
"Your last output was not valid XML.\n" +
"Convert the intended action into VALID XML tool calls or <no_action/>.\n" +
"Output XML only. No commentary.\n" +
"You MUST use only action tools.\n" +
"\nPrevious output:\n" + TrimForPrompt(actionResponse, 600);
string fixedResponse = await client.GetChatCompletionAsync(fixInstruction, actionContext, maxTokens: 128, temperature: 0.1f);
if (!string.IsNullOrEmpty(fixedResponse) && IsXmlToolCall(fixedResponse))
{
actionResponse = fixedResponse;
}
else
{
if (Prefs.DevMode)
{
WulaLog.Debug("[WulaAI] Turn 2/3 conversion failed; treating as <no_action/>");
}
actionResponse = "<no_action/>";
} }
actionResponse = "<no_action/>";
} }
PhaseExecutionResult actionResult = await ExecuteXmlToolsForPhase(actionResponse, actionPhase); PhaseExecutionResult actionResult = await ExecuteXmlToolsForPhase(actionResponse, actionPhase);
@@ -569,8 +639,9 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
SetThinkingPhase(2, true); SetThinkingPhase(2, true);
string retryActionInstruction = GetToolSystemInstruction(actionPhase) + string retryActionInstruction = GetToolSystemInstruction(actionPhase) +
"\n\n# RETRY\nYou chose to retry. Output XML tool calls only (or <no_action/>)."; "\n\n# RETRY\nYou chose to retry. Output XML tool calls only (or <no_action/>).";
string retryActionResponse = await client.GetChatCompletionAsync(retryActionInstruction, BuildToolContext(), maxTokens: 128, temperature: 0.1f); var retryActionContext = BuildToolContext(actionPhase, includeUser: true);
string retryActionResponse = await client.GetChatCompletionAsync(retryActionInstruction, retryActionContext, maxTokens: 128, temperature: 0.1f);
if (string.IsNullOrEmpty(retryActionResponse)) if (string.IsNullOrEmpty(retryActionResponse))
{ {
_currentResponse = "Wula_AI_Error_ConnectionLost".Translate(); _currentResponse = "Wula_AI_Error_ConnectionLost".Translate();
@@ -581,9 +652,28 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
{ {
if (Prefs.DevMode) if (Prefs.DevMode)
{ {
WulaLog.Debug("[WulaAI] Retry action phase missing XML; treating as <no_action/>"); WulaLog.Debug("[WulaAI] Retry action phase missing XML; attempting XML-only conversion.");
}
string retryFixInstruction = retryActionInstruction +
"\n\n# FORMAT FIX\n" +
"Your last output was not valid XML.\n" +
"Convert the intended action into VALID XML tool calls or <no_action/>.\n" +
"Output XML only. No commentary.\n" +
"You MUST use only action tools.\n" +
"\nPrevious output:\n" + TrimForPrompt(retryActionResponse, 600);
string retryFixedResponse = await client.GetChatCompletionAsync(retryFixInstruction, retryActionContext, maxTokens: 128, temperature: 0.1f);
if (!string.IsNullOrEmpty(retryFixedResponse) && IsXmlToolCall(retryFixedResponse))
{
retryActionResponse = retryFixedResponse;
}
else
{
if (Prefs.DevMode)
{
WulaLog.Debug("[WulaAI] Retry action conversion failed; treating as <no_action/>");
}
retryActionResponse = "<no_action/>";
} }
retryActionResponse = "<no_action/>";
} }
actionResult = await ExecuteXmlToolsForPhase(retryActionResponse, actionPhase); actionResult = await ExecuteXmlToolsForPhase(retryActionResponse, actionPhase);
@@ -622,14 +712,31 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
replyInstruction += "\nIMPORTANT: An action tool failed. You MUST acknowledge the failure and MUST NOT claim success."; replyInstruction += "\nIMPORTANT: An action tool failed. You MUST acknowledge the failure and MUST NOT claim success.";
} }
string reply = await client.GetChatCompletionAsync(replyInstruction, _history); string reply = await client.GetChatCompletionAsync(replyInstruction, BuildReplyHistory());
if (string.IsNullOrEmpty(reply)) if (string.IsNullOrEmpty(reply))
{ {
_currentResponse = "Wula_AI_Error_ConnectionLost".Translate(); _currentResponse = "Wula_AI_Error_ConnectionLost".Translate();
return; return;
} }
if (IsXmlToolCall(reply)) bool replyHadXml = IsXmlToolCall(reply);
string strippedReply = StripXmlTags(reply)?.Trim() ?? "";
if (replyHadXml || 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";
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() ?? "";
}
}
if (replyHadXml)
{ {
string cleaned = StripXmlTags(reply)?.Trim() ?? ""; string cleaned = StripXmlTags(reply)?.Trim() ?? "";
if (string.IsNullOrWhiteSpace(cleaned)) if (string.IsNullOrWhiteSpace(cleaned))
@@ -682,21 +789,6 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
return default; return default;
} }
static bool IsActionToolName(string toolName)
{
return toolName == "spawn_resources" ||
toolName == "send_reinforcement" ||
toolName == "call_bombardment" ||
toolName == "modify_goodwill";
}
static bool IsQueryToolName(string toolName)
{
if (string.IsNullOrWhiteSpace(toolName)) return false;
return toolName.StartsWith("get_", StringComparison.OrdinalIgnoreCase) ||
toolName.StartsWith("search_", StringComparison.OrdinalIgnoreCase);
}
int maxTools = MaxToolsPerPhase(phase); int maxTools = MaxToolsPerPhase(phase);
int executed = 0; int executed = 0;
bool executedActionTool = false; bool executedActionTool = false;
@@ -920,6 +1012,49 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
return stripped; return stripped;
} }
private List<(string role, string message)> BuildReplyHistory()
{
if (_history == null || _history.Count == 0) return new List<(string role, string message)>();
int lastUserIndex = -1;
for (int i = _history.Count - 1; i >= 0; i--)
{
if (string.Equals(_history[i].role, "user", StringComparison.OrdinalIgnoreCase))
{
lastUserIndex = i;
break;
}
}
var filtered = new List<(string role, string message)>();
for (int i = 0; i < _history.Count; i++)
{
var entry = _history[i];
if (string.Equals(entry.role, "tool", StringComparison.OrdinalIgnoreCase))
{
if (lastUserIndex != -1 && i > lastUserIndex)
{
filtered.Add(entry);
}
continue;
}
if (!string.Equals(entry.role, "assistant", StringComparison.OrdinalIgnoreCase))
{
filtered.Add(entry);
continue;
}
string stripped = StripXmlTags(entry.message)?.Trim() ?? "";
if (!string.IsNullOrWhiteSpace(stripped))
{
filtered.Add(entry);
}
}
return filtered;
}
private string StripExpressionTags(string text) private string StripExpressionTags(string text)
{ {
if (string.IsNullOrEmpty(text)) return text; if (string.IsNullOrEmpty(text)) return text;