This commit is contained in:
2025-12-31 12:28:02 +08:00
parent 909dea4ffb
commit 2af305de68
3 changed files with 79 additions and 9 deletions

View File

@@ -113,7 +113,8 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
- Do NOT split multi-item requests across turns. - Do NOT split multi-item requests across turns.
5. **TOOLS**: You MAY call any tools listed in ""# TOOLS (AVAILABLE)"". 5. **TOOLS**: You MAY call any tools listed in ""# TOOLS (AVAILABLE)"".
6. **ANTI-HALLUCINATION**: Never invent tools, parameters, defNames, coordinates, or tool results. If a tool is needed but not available, output { ""tool_calls"": [] }. 6. **ANTI-HALLUCINATION**: Never invent tools, parameters, defNames, coordinates, or tool results. If a tool is needed but not available, output { ""tool_calls"": [] }.
7. **NO TAGS**: Do NOT use <think> tags, code fences, or any extra text outside JSON."; 7. **STEP BUDGET (OPTIONAL)**: You MAY include { ""meta"": { ""step_budget"": <positive integer> } } to request more tool steps for this turn.
8. **NO TAGS**: Do NOT use <think> tags, code fences, or any extra text outside JSON.";
public AIIntelligenceCore(World world) : base(world) public AIIntelligenceCore(World world) : base(world)
{ {
@@ -923,14 +924,27 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
return false; return false;
} }
private static int? ExtractStepBudget(string json)
{
if (string.IsNullOrWhiteSpace(json)) return null;
var match = Regex.Match(json, "\"step_budget\"\\s*:\\s*\"?(\\d+)\"?", RegexOptions.IgnoreCase);
if (!match.Success) return null;
if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))
{
return value;
}
return null;
}
private static string BuildReactFormatFixInstruction(string previousOutput) private static string BuildReactFormatFixInstruction(string previousOutput)
{ {
return "# FORMAT FIX (REACT JSON ONLY)\n" + return "# FORMAT FIX (REACT JSON ONLY)\n" +
"Output valid JSON with fields thought/tool_calls.\n" + "Output valid JSON with fields thought/tool_calls.\n" +
"If tools are needed, tool_calls must be non-empty.\n" + "If tools are needed, tool_calls must be non-empty.\n" +
"If no tools are needed, output exactly: {\"tool_calls\": []} (you may include thought).\n" + "If no tools are needed, output exactly: {\"tool_calls\": []} (you may include thought).\n" +
"You MAY include an optional meta block: {\"meta\":{\"step_budget\":<positive integer>}}.\n" +
"Do NOT output any text outside JSON.\n" + "Do NOT output any text outside JSON.\n" +
"Schema: {\"thought\":\"...\",\"tool_calls\":[{\"type\":\"function\",\"function\":{\"name\":\"tool_name\",\"arguments\":{...}}}]}\n" + "Schema: {\"thought\":\"...\",\"tool_calls\":[{\"type\":\"function\",\"function\":{\"name\":\"tool_name\",\"arguments\":{...}}}],\"meta\":{\"step_budget\":7}}\n" +
"\nPrevious output:\n" + TrimForPrompt(previousOutput, 600); "\nPrevious output:\n" + TrimForPrompt(previousOutput, 600);
} }
@@ -964,8 +978,9 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
"You used tool names that are NOT available: " + invalidList + "\n" + "You used tool names that are NOT available: " + invalidList + "\n" +
"Re-emit JSON with only available tools from # TOOLS (AVAILABLE).\n" + "Re-emit JSON with only available tools from # TOOLS (AVAILABLE).\n" +
"If no tools are needed, output exactly: {\"tool_calls\": []}.\n" + "If no tools are needed, output exactly: {\"tool_calls\": []}.\n" +
"You MAY include an optional meta block: {\"meta\":{\"step_budget\":<positive integer>}}.\n" +
"Do NOT output any text outside JSON.\n" + "Do NOT output any text outside JSON.\n" +
"Schema: {\"thought\":\"...\",\"tool_calls\":[{\"type\":\"function\",\"function\":{\"name\":\"tool_name\",\"arguments\":{...}}}]}"; "Schema: {\"thought\":\"...\",\"tool_calls\":[{\"type\":\"function\",\"function\":{\"name\":\"tool_name\",\"arguments\":{...}}}],\"meta\":{\"step_budget\":7}}";
} }
private static bool ShouldRetryTools(string response) private static bool ShouldRetryTools(string response)
@@ -1832,13 +1847,16 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
int maxSteps = DefaultReactMaxSteps; int maxSteps = DefaultReactMaxSteps;
float maxSeconds = DefaultReactMaxSeconds; float maxSeconds = DefaultReactMaxSeconds;
int stepBudgetMax = int.MaxValue;
if (settings != null) if (settings != null)
{ {
maxSteps = Math.Max(1, Math.Min(10, settings.reactMaxSteps)); stepBudgetMax = Math.Max(1, settings.reactMaxStepsMax);
maxSeconds = Mathf.Clamp(settings.reactMaxSeconds, 2f, 60f); maxSteps = Math.Max(1, settings.reactMaxSteps);
maxSeconds = Math.Max(2f, settings.reactMaxSeconds);
} }
_thinkingPhaseTotal = maxSteps; _thinkingPhaseTotal = maxSteps;
string toolPhaseReplyCandidate = null; string toolPhaseReplyCandidate = null;
bool budgetApplied = false;
for (int step = 1; step <= maxSteps; step++) for (int step = 1; step <= maxSteps; step++)
{ {
if (Time.realtimeSinceStartup - startTime > maxSeconds) if (Time.realtimeSinceStartup - startTime > maxSeconds)
@@ -1862,6 +1880,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
} }
string normalizedResponse = NormalizeReactResponse(response); string normalizedResponse = NormalizeReactResponse(response);
string parsedSource = normalizedResponse;
if (!JsonToolCallParser.TryParseToolCallsFromText(normalizedResponse, out var toolCalls, out string jsonFragment)) if (!JsonToolCallParser.TryParseToolCallsFromText(normalizedResponse, out var toolCalls, out string jsonFragment))
{ {
if (LooksLikeNaturalReply(normalizedResponse)) if (LooksLikeNaturalReply(normalizedResponse))
@@ -1890,6 +1909,27 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
} }
break; break;
} }
parsedSource = normalizedFixed;
}
if (!budgetApplied && !string.IsNullOrWhiteSpace(jsonFragment))
{
string budgetSource = jsonFragment;
int? requested = ExtractStepBudget(budgetSource);
if (!requested.HasValue)
{
requested = ExtractStepBudget(parsedSource);
}
if (requested.HasValue)
{
budgetApplied = true;
int clamped = Math.Max(1, Math.Min(stepBudgetMax, requested.Value));
if (clamped > maxSteps)
{
maxSteps = clamped;
_thinkingPhaseTotal = maxSteps;
}
}
} }
var invalidTools = toolCalls var invalidTools = toolCalls
@@ -1907,9 +1947,30 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
string correctionInstruction = BuildReactToolCorrectionInstruction(invalidTools); string correctionInstruction = BuildReactToolCorrectionInstruction(invalidTools);
string correctedResponse = await client.GetChatCompletionAsync(correctionInstruction, reactContext, maxTokens: 1024, temperature: 0.1f, base64Image: base64Image); string correctedResponse = await client.GetChatCompletionAsync(correctionInstruction, reactContext, maxTokens: 1024, temperature: 0.1f, base64Image: base64Image);
string normalizedCorrected = NormalizeReactResponse(correctedResponse); string normalizedCorrected = NormalizeReactResponse(correctedResponse);
if (!string.IsNullOrEmpty(normalizedCorrected) && if (!string.IsNullOrEmpty(normalizedCorrected) &&
JsonToolCallParser.TryParseToolCallsFromText(normalizedCorrected, out toolCalls, out jsonFragment)) JsonToolCallParser.TryParseToolCallsFromText(normalizedCorrected, out toolCalls, out jsonFragment))
{ {
if (!budgetApplied && !string.IsNullOrWhiteSpace(jsonFragment))
{
string budgetSource = jsonFragment;
int? requested = ExtractStepBudget(budgetSource);
if (!requested.HasValue)
{
requested = ExtractStepBudget(normalizedCorrected);
}
if (requested.HasValue)
{
budgetApplied = true;
int clamped = Math.Max(1, Math.Min(stepBudgetMax, requested.Value));
if (clamped > maxSteps)
{
maxSteps = clamped;
_thinkingPhaseTotal = maxSteps;
}
}
}
parsedSource = normalizedCorrected;
invalidTools = toolCalls invalidTools = toolCalls
.Where(c => !IsToolAvailable(c.Name)) .Where(c => !IsToolAvailable(c.Name))
.Select(c => c.Name) .Select(c => c.Name)
@@ -2046,7 +2107,9 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
private async Task<PhaseExecutionResult> ExecuteJsonToolsForStep(string json) private async Task<PhaseExecutionResult> ExecuteJsonToolsForStep(string json)
{ {
string guidance = "ToolRunner Guidance: Continue with JSON only using {\"thought\":\"...\",\"tool_calls\":[...]}. " + string guidance = "ToolRunner Guidance: Continue with JSON only using {\"thought\":\"...\",\"tool_calls\":[...]}. " +
"If no more tools are needed, output exactly: {\"tool_calls\": []}. Do NOT output any text outside JSON."; "If no more tools are needed, output exactly: {\"tool_calls\": []}. " +
"You MAY include {\"meta\":{\"step_budget\":<positive integer>}} to request more steps. " +
"Do NOT output any text outside JSON.";
if (!JsonToolCallParser.TryParseToolCallsFromText(json ?? "", out var toolCalls, out string jsonFragment)) if (!JsonToolCallParser.TryParseToolCallsFromText(json ?? "", out var toolCalls, out string jsonFragment))
{ {

View File

@@ -15,6 +15,7 @@ namespace WulaFallenEmpire
public static bool _showVlmApiKey = false; public static bool _showVlmApiKey = false;
private string _maxContextTokensBuffer; private string _maxContextTokensBuffer;
private string _reactMaxStepsBuffer; private string _reactMaxStepsBuffer;
private string _reactMaxStepsMaxBuffer;
private string _reactMaxSecondsBuffer; private string _reactMaxSecondsBuffer;
public WulaFallenEmpireMod(ModContentPack content) : base(content) public WulaFallenEmpireMod(ModContentPack content) : base(content)
@@ -98,9 +99,13 @@ namespace WulaFallenEmpire
listingStandard.GapLine(); listingStandard.GapLine();
listingStandard.Label("<color=cyan>ReAct Loop Settings</color>"); listingStandard.Label("<color=cyan>ReAct Loop Settings</color>");
listingStandard.Label("Max Steps (1-10):"); listingStandard.Label("Default Steps:");
Rect stepsRect = listingStandard.GetRect(Text.LineHeight); Rect stepsRect = listingStandard.GetRect(Text.LineHeight);
Widgets.TextFieldNumeric(stepsRect, ref settings.reactMaxSteps, ref _reactMaxStepsBuffer, 1, 20); Widgets.TextFieldNumeric(stepsRect, ref settings.reactMaxSteps, ref _reactMaxStepsBuffer, 1, int.MaxValue);
listingStandard.Label("Max Steps Limit (step_budget upper bound):");
Rect stepsMaxRect = listingStandard.GetRect(Text.LineHeight);
Widgets.TextFieldNumeric(stepsMaxRect, ref settings.reactMaxStepsMax, ref _reactMaxStepsMaxBuffer, 1, int.MaxValue);
listingStandard.Label("Max Seconds (2-60):"); listingStandard.Label("Max Seconds (2-60):");
Rect secondsRect = listingStandard.GetRect(Text.LineHeight); Rect secondsRect = listingStandard.GetRect(Text.LineHeight);

View File

@@ -24,6 +24,7 @@ namespace WulaFallenEmpire
public bool commentOnNegativeOnly = false; public bool commentOnNegativeOnly = false;
public string extraPersonalityPrompt = ""; public string extraPersonalityPrompt = "";
public int reactMaxSteps = 4; public int reactMaxSteps = 4;
public int reactMaxStepsMax = 7;
public float reactMaxSeconds = 60f; public float reactMaxSeconds = 60f;
public bool showReactTraceInUI = false; public bool showReactTraceInUI = false;
@@ -48,6 +49,7 @@ namespace WulaFallenEmpire
Scribe_Values.Look(ref commentOnNegativeOnly, "commentOnNegativeOnly", false); Scribe_Values.Look(ref commentOnNegativeOnly, "commentOnNegativeOnly", false);
Scribe_Values.Look(ref extraPersonalityPrompt, "extraPersonalityPrompt", ""); Scribe_Values.Look(ref extraPersonalityPrompt, "extraPersonalityPrompt", "");
Scribe_Values.Look(ref reactMaxSteps, "reactMaxSteps", 4); Scribe_Values.Look(ref reactMaxSteps, "reactMaxSteps", 4);
Scribe_Values.Look(ref reactMaxStepsMax, "reactMaxStepsMax", 7);
Scribe_Values.Look(ref reactMaxSeconds, "reactMaxSeconds", 60f); Scribe_Values.Look(ref reactMaxSeconds, "reactMaxSeconds", 60f);
Scribe_Values.Look(ref showReactTraceInUI, "showReactTraceInUI", false); Scribe_Values.Look(ref showReactTraceInUI, "showReactTraceInUI", false);