Merge branch 'main' of https://git.ra3battle.cn/Kalospacer/WulaFallenEmpireRW
This commit is contained in:
@@ -10,8 +10,16 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
public class Tool_GetMapPawns : AITool
|
||||
{
|
||||
public override string Name => "get_map_pawns";
|
||||
public override string Description => "Scans the current map and lists pawns. Supports filtering by relation (friendly/hostile/neutral), type (colonist/animal/mechanoid/humanlike), and status (prisoner/slave/guest/downed).";
|
||||
public override string UsageSchema => "<get_map_pawns><filter>string (optional, comma-separated: friendly, hostile, neutral, colonist, animal, mech, humanlike, prisoner, slave, guest, wild, downed)</filter><maxResults>int (optional, default 50)</maxResults></get_map_pawns>";
|
||||
public override string Description => "Scans the current map and lists pawns (including corpses). Supports filtering by relation (friendly/hostile/neutral), type (colonist/animal/mech/humanlike), and status (prisoner/slave/guest/wild/downed/dead).";
|
||||
public override string UsageSchema =>
|
||||
"<get_map_pawns><filter>string (optional, comma-separated: friendly, hostile, neutral, colonist, animal, mech, humanlike, prisoner, slave, guest, wild, downed, dead)</filter><includeDead>true/false (optional, default true)</includeDead><maxResults>int (optional, default 50)</maxResults></get_map_pawns>";
|
||||
|
||||
private struct MapPawnEntry
|
||||
{
|
||||
public Pawn Pawn;
|
||||
public bool IsDead;
|
||||
public IntVec3 Position;
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
@@ -21,36 +29,85 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
|
||||
string filterRaw = null;
|
||||
if (parsed.TryGetValue("filter", out string f)) filterRaw = f;
|
||||
|
||||
int maxResults = 50;
|
||||
if (parsed.TryGetValue("maxResults", out string maxStr) && int.TryParse(maxStr, out int mr))
|
||||
{
|
||||
maxResults = Math.Max(1, Math.Min(200, mr));
|
||||
}
|
||||
|
||||
bool includeDead = true;
|
||||
if (parsed.TryGetValue("includeDead", out string includeDeadStr) && bool.TryParse(includeDeadStr, out bool parsedIncludeDead))
|
||||
{
|
||||
includeDead = parsedIncludeDead;
|
||||
}
|
||||
|
||||
Map map = Find.CurrentMap;
|
||||
if (map == null) return "Error: No active map.";
|
||||
|
||||
var filters = ParseFilters(filterRaw);
|
||||
if (filters.Contains("dead")) includeDead = true;
|
||||
|
||||
List<Pawn> pawns = map.mapPawns?.AllPawnsSpawned?.Where(p => p != null).ToList() ?? new List<Pawn>();
|
||||
pawns = pawns.Where(p => MatchesFilters(p, filters)).ToList();
|
||||
var entries = new List<MapPawnEntry>();
|
||||
|
||||
if (pawns.Count == 0) return "No pawns matched.";
|
||||
var livePawns = map.mapPawns?.AllPawnsSpawned?.Where(p => p != null).ToList() ?? new List<Pawn>();
|
||||
foreach (var pawn in livePawns)
|
||||
{
|
||||
entries.Add(new MapPawnEntry
|
||||
{
|
||||
Pawn = pawn,
|
||||
IsDead = pawn.Dead,
|
||||
Position = pawn.Position
|
||||
});
|
||||
}
|
||||
|
||||
pawns = pawns
|
||||
.OrderByDescending(p => IsHostileToPlayer(p))
|
||||
.ThenByDescending(p => p.RaceProps?.Humanlike ?? false)
|
||||
.ThenBy(p => p.def?.label ?? "")
|
||||
.ThenBy(p => p.Name?.ToStringShort ?? "")
|
||||
if (includeDead && map.listerThings != null)
|
||||
{
|
||||
var corpses = map.listerThings.ThingsInGroup(ThingRequestGroup.Corpse);
|
||||
if (corpses != null)
|
||||
{
|
||||
foreach (var thing in corpses)
|
||||
{
|
||||
if (thing is not Corpse corpse) continue;
|
||||
Pawn inner = corpse.InnerPawn;
|
||||
if (inner == null) continue;
|
||||
|
||||
entries.Add(new MapPawnEntry
|
||||
{
|
||||
Pawn = inner,
|
||||
IsDead = true,
|
||||
Position = corpse.Position
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entries = entries
|
||||
.Where(e => e.Pawn != null)
|
||||
.GroupBy(e => e.Pawn.thingIDNumber)
|
||||
.Select(g => g.First())
|
||||
.Where(e => includeDead || !e.IsDead)
|
||||
.Where(e => MatchesFilters(e, filters))
|
||||
.ToList();
|
||||
|
||||
if (entries.Count == 0) return "No pawns matched.";
|
||||
|
||||
int matched = entries.Count;
|
||||
var selected = entries
|
||||
.OrderByDescending(e => IsHostileToPlayer(e.Pawn))
|
||||
.ThenBy(e => e.IsDead) // living first
|
||||
.ThenByDescending(e => e.Pawn.RaceProps?.Humanlike ?? false)
|
||||
.ThenBy(e => e.Pawn.def?.label ?? "")
|
||||
.ThenBy(e => e.Pawn.Name?.ToStringShort ?? "")
|
||||
.Take(maxResults)
|
||||
.ToList();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine($"Found {pawns.Count} pawns on map (showing up to {maxResults}):");
|
||||
sb.AppendLine($"Found {matched} matching pawns on map (showing {selected.Count}):");
|
||||
|
||||
foreach (var pawn in pawns)
|
||||
foreach (var entry in selected)
|
||||
{
|
||||
AppendPawnLine(sb, pawn);
|
||||
AppendPawnLine(sb, entry);
|
||||
}
|
||||
|
||||
return sb.ToString().TrimEnd();
|
||||
@@ -66,7 +123,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (string.IsNullOrWhiteSpace(filterRaw)) return set;
|
||||
|
||||
var parts = filterRaw.Split(new[] { ',', ',', ';', '、', '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var parts = filterRaw.Split(new[] { ',', '\uFF0C', ';', '\u3001', '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
string token = part.Trim().ToLowerInvariant();
|
||||
@@ -85,16 +142,18 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
else if (token == "访客" || token == "客人") token = "guest";
|
||||
else if (token == "野生") token = "wild";
|
||||
else if (token == "倒地" || token == "昏迷") token = "downed";
|
||||
else if (token == "死亡" || token == "尸体") token = "dead";
|
||||
|
||||
set.Add(token);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
private static bool MatchesFilters(Pawn pawn, HashSet<string> filters)
|
||||
private static bool MatchesFilters(MapPawnEntry entry, HashSet<string> filters)
|
||||
{
|
||||
if (filters == null || filters.Count == 0) return true;
|
||||
|
||||
Pawn pawn = entry.Pawn;
|
||||
bool anyMatched = false;
|
||||
foreach (var f in filters)
|
||||
{
|
||||
@@ -112,6 +171,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
"guest" => pawn.guest != null && pawn.Faction != null && pawn.Faction != Faction.OfPlayer,
|
||||
"wild" => pawn.Faction == null && (pawn.RaceProps?.Animal ?? false),
|
||||
"downed" => pawn.Downed,
|
||||
"dead" => entry.IsDead || pawn.Dead,
|
||||
_ => false
|
||||
};
|
||||
|
||||
@@ -137,19 +197,20 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
private static bool IsNeutralToPlayer(Pawn pawn)
|
||||
{
|
||||
if (pawn == null || Faction.OfPlayer == null) return false;
|
||||
if (pawn.Faction == null) return true; // wild/animals etc.
|
||||
if (pawn.Faction == null) return true;
|
||||
if (pawn.Faction == Faction.OfPlayer) return false;
|
||||
return !pawn.HostileTo(Faction.OfPlayer);
|
||||
}
|
||||
|
||||
private static void AppendPawnLine(StringBuilder sb, Pawn pawn)
|
||||
private static void AppendPawnLine(StringBuilder sb, MapPawnEntry entry)
|
||||
{
|
||||
Pawn pawn = entry.Pawn;
|
||||
string name = pawn.Name?.ToStringShort ?? pawn.LabelShortCap;
|
||||
string kind = pawn.def?.label ?? "unknown";
|
||||
string faction = pawn.Faction?.Name ?? (pawn.RaceProps?.Animal == true ? "Wild" : "None");
|
||||
string relation = IsHostileToPlayer(pawn) ? "Hostile" : (pawn.Faction == Faction.OfPlayer ? "Player" : "Non-hostile");
|
||||
string tags = BuildTags(pawn);
|
||||
string pos = pawn.Position.IsValid ? pawn.Position.ToString() : "?";
|
||||
string tags = BuildTags(pawn, entry.IsDead);
|
||||
string pos = entry.Position.IsValid ? entry.Position.ToString() : (pawn.Position.IsValid ? pawn.Position.ToString() : "?");
|
||||
|
||||
sb.Append($"- {name} ({kind})");
|
||||
sb.Append($" faction={faction} relation={relation} pos={pos}");
|
||||
@@ -157,7 +218,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
private static string BuildTags(Pawn pawn)
|
||||
private static string BuildTags(Pawn pawn, bool isDead)
|
||||
{
|
||||
var tags = new List<string>();
|
||||
if (pawn.IsFreeColonist) tags.Add("colonist");
|
||||
@@ -165,6 +226,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
if (pawn.IsSlaveOfColony) tags.Add("slave");
|
||||
if (pawn.guest != null && pawn.Faction != null && pawn.Faction != Faction.OfPlayer) tags.Add("guest");
|
||||
if (pawn.Downed) tags.Add("downed");
|
||||
if (isDead || pawn.Dead) tags.Add("dead");
|
||||
if (pawn.InMentalState) tags.Add("mental");
|
||||
if (pawn.Drafted) tags.Add("drafted");
|
||||
if (pawn.RaceProps?.Humanlike ?? false) tags.Add("humanlike");
|
||||
@@ -174,3 +236,4 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
{
|
||||
public class Tool_GetRecentNotifications : AITool
|
||||
{
|
||||
public override string Name => "get_recent_notifications";
|
||||
public override string Description => "Returns the most recent letters and messages, sorted by in-game time from newest to oldest.";
|
||||
public override string UsageSchema =>
|
||||
"<get_recent_notifications><count>int (optional, default 10, max 100)</count><includeLetters>true/false (optional, default true)</includeLetters><includeMessages>true/false (optional, default true)</includeMessages></get_recent_notifications>";
|
||||
|
||||
private struct NotificationEntry
|
||||
{
|
||||
public int Tick;
|
||||
public string Kind;
|
||||
public string Title;
|
||||
public string Body;
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = 10;
|
||||
bool includeLetters = true;
|
||||
bool includeMessages = true;
|
||||
|
||||
var parsed = ParseXmlArgs(args);
|
||||
if (parsed.TryGetValue("count", out var countStr) && int.TryParse(countStr, out int parsedCount))
|
||||
{
|
||||
count = parsedCount;
|
||||
}
|
||||
|
||||
if (parsed.TryGetValue("includeLetters", out var incLettersStr) && bool.TryParse(incLettersStr, out bool parsedLetters))
|
||||
{
|
||||
includeLetters = parsedLetters;
|
||||
}
|
||||
|
||||
if (parsed.TryGetValue("includeMessages", out var incMessagesStr) && bool.TryParse(incMessagesStr, out bool parsedMessages))
|
||||
{
|
||||
includeMessages = parsedMessages;
|
||||
}
|
||||
|
||||
count = Math.Max(1, Math.Min(100, count));
|
||||
|
||||
int now = Find.TickManager?.TicksGame ?? 0;
|
||||
var entries = new List<NotificationEntry>();
|
||||
|
||||
if (includeLetters)
|
||||
{
|
||||
entries.AddRange(ReadLetters(now));
|
||||
}
|
||||
|
||||
if (includeMessages)
|
||||
{
|
||||
entries.AddRange(ReadMessages(now));
|
||||
}
|
||||
|
||||
if (entries.Count == 0)
|
||||
{
|
||||
return "No recent letters or messages found.";
|
||||
}
|
||||
|
||||
var selected = entries
|
||||
.OrderByDescending(e => e.Tick)
|
||||
.ThenByDescending(e => e.Kind)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine($"Found {selected.Count} recent notifications (newest -> oldest):");
|
||||
|
||||
int idx = 1;
|
||||
foreach (var e in selected)
|
||||
{
|
||||
sb.AppendLine($"{idx}. [{e.Kind}] tick={e.Tick}");
|
||||
if (!string.IsNullOrWhiteSpace(e.Title))
|
||||
{
|
||||
sb.AppendLine($" Title: {TrimForDisplay(e.Title, 140)}");
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(e.Body))
|
||||
{
|
||||
sb.AppendLine($" Text: {TrimForDisplay(e.Body, 600)}");
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<NotificationEntry> ReadLetters(int fallbackNow)
|
||||
{
|
||||
var list = new List<NotificationEntry>();
|
||||
|
||||
object letterStack = Find.LetterStack;
|
||||
if (letterStack == null) return list;
|
||||
|
||||
IEnumerable letters = null;
|
||||
try
|
||||
{
|
||||
letters = GetMemberValue(letterStack, "LettersListForReading", "letters", "lettersList", "lettersListForReading") as IEnumerable;
|
||||
}
|
||||
catch
|
||||
{
|
||||
letters = null;
|
||||
}
|
||||
|
||||
if (letters == null) return list;
|
||||
|
||||
foreach (var letter in letters)
|
||||
{
|
||||
if (letter == null) continue;
|
||||
|
||||
int tick = GetInt(letter, "arrivalTick", "receivedTick", "tick", "ticksGame") ?? fallbackNow;
|
||||
string label = GetString(letter, "label", "Label", "LabelCap");
|
||||
string text = GetString(letter, "text", "Text", "TextString", "LetterText");
|
||||
|
||||
string defName = GetString(GetMemberValue(letter, "def", "Def"), "defName", "DefName");
|
||||
string kind = string.IsNullOrWhiteSpace(defName) ? "Letter" : $"Letter:{defName}";
|
||||
|
||||
list.Add(new NotificationEntry
|
||||
{
|
||||
Tick = tick,
|
||||
Kind = kind,
|
||||
Title = label,
|
||||
Body = text
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static IEnumerable<NotificationEntry> ReadMessages(int fallbackNow)
|
||||
{
|
||||
var list = new List<NotificationEntry>();
|
||||
|
||||
IEnumerable messages = null;
|
||||
try
|
||||
{
|
||||
messages = GetMemberValue(typeof(Messages), "MessagesListForReading", "messagesListForReading", "messages") as IEnumerable;
|
||||
}
|
||||
catch
|
||||
{
|
||||
messages = null;
|
||||
}
|
||||
|
||||
if (messages == null) return list;
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (message == null) continue;
|
||||
|
||||
int tick = GetInt(message, "time", "timeReceived", "receivedTick", "ticks", "tick", "startTick") ?? fallbackNow;
|
||||
string text = GetString(message, "text", "Text", "message", "Message");
|
||||
string typeDef = GetString(GetMemberValue(message, "def", "Def", "type", "Type", "messageType", "MessageType"), "defName", "DefName");
|
||||
string kind = string.IsNullOrWhiteSpace(typeDef) ? "Message" : $"Message:{typeDef}";
|
||||
|
||||
list.Add(new NotificationEntry
|
||||
{
|
||||
Tick = tick,
|
||||
Kind = kind,
|
||||
Title = null,
|
||||
Body = text
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static string TrimForDisplay(string s, int maxChars)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return s;
|
||||
string oneLine = s.Replace("\r", " ").Replace("\n", " ").Trim();
|
||||
if (oneLine.Length <= maxChars) return oneLine;
|
||||
return oneLine.Substring(0, maxChars) + "...";
|
||||
}
|
||||
|
||||
private static object GetMemberValue(object objOrType, params string[] names)
|
||||
{
|
||||
if (objOrType == null || names == null || names.Length == 0) return null;
|
||||
|
||||
Type t = objOrType as Type ?? objOrType.GetType();
|
||||
bool isStatic = objOrType is Type;
|
||||
|
||||
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic |
|
||||
BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Static;
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name)) continue;
|
||||
|
||||
var prop = t.GetProperty(name, Flags);
|
||||
if (prop != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return prop.GetValue(isStatic ? null : objOrType, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
var field = t.GetField(name, Flags);
|
||||
if (field != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return field.GetValue(isStatic ? null : objOrType);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int? GetInt(object obj, params string[] names)
|
||||
{
|
||||
object val = GetMemberValue(obj, names);
|
||||
if (val == null) return null;
|
||||
if (val is int i) return i;
|
||||
if (val is long l)
|
||||
{
|
||||
if (l > int.MaxValue) return int.MaxValue;
|
||||
if (l < int.MinValue) return int.MinValue;
|
||||
return (int)l;
|
||||
}
|
||||
if (val is float f) return (int)f;
|
||||
if (val is double d) return (int)d;
|
||||
if (int.TryParse(val.ToString(), out int parsed)) return parsed;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GetString(object obj, params string[] names)
|
||||
{
|
||||
object val = GetMemberValue(obj, names);
|
||||
if (val == null) return null;
|
||||
return val.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,17 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
||||
private bool _scrollToBottom = false;
|
||||
private List<AITool> _tools = new List<AITool>();
|
||||
private Dictionary<int, Texture2D> _portraits = new Dictionary<int, Texture2D>();
|
||||
private const int MaxHistoryTokens = 100000;
|
||||
private const int DefaultMaxHistoryTokens = 100000;
|
||||
private const int CharsPerToken = 4;
|
||||
private int _continuationDepth = 0;
|
||||
private const int MaxContinuationDepth = 6;
|
||||
|
||||
private static int GetMaxHistoryTokens()
|
||||
{
|
||||
int configured = WulaFallenEmpire.WulaFallenEmpireMod.settings?.maxContextTokens ?? DefaultMaxHistoryTokens;
|
||||
return Math.Max(1000, Math.Min(200000, configured));
|
||||
}
|
||||
|
||||
// Static instance for tools to access
|
||||
public static Dialog_AIConversation Instance { get; private set; }
|
||||
|
||||
@@ -51,6 +57,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
3. **WORKFLOW**: You must use tools step-by-step to accomplish tasks. Use the output from one tool to inform your next step.
|
||||
4. **ANTI-HALLUCINATION**: You MUST ONLY call tools from the list below. Do NOT invent tools or parameters. If a task is impossible, explain why without calling a tool.
|
||||
5. **ENFORCEMENT**: The game will execute multiple info tools in one response, but it will NOT execute an action tool (spawn/bombardment/reinforcements/goodwill/expression) if you also included info tools in the same response. Call action tools in a separate turn after you see the info tool results.
|
||||
6. **AFTER TOOL RESULTS**: After you receive tool results, if no further tools are needed, you MUST reply in natural language only (no XML).
|
||||
|
||||
====
|
||||
|
||||
@@ -151,34 +158,48 @@ Example:
|
||||
<get_colonist_status/>
|
||||
|
||||
## get_map_resources
|
||||
Description: Checks the player's map for specific resources or buildings to verify their inventory.
|
||||
Description: Checks the player's map for specific resources or buildings to verify their inventory.
|
||||
Use this tool when:
|
||||
- The player claims they are lacking a specific resource (e.g., ""we need steel,"" ""we have no food"").
|
||||
- You want to assess the colony's material wealth before making a decision.
|
||||
Parameters:
|
||||
- resourceName: (OPTIONAL) The specific `ThingDef` name of the resource to check (e.g., 'Steel', 'MealSimple'). If omitted, provides a general overview.
|
||||
Usage:
|
||||
<get_map_resources>
|
||||
<resourceName>optional resource name</resourceName>
|
||||
</get_map_resources>
|
||||
Example (checking for Steel):
|
||||
<get_map_resources>
|
||||
<resourceName>Steel</resourceName>
|
||||
</get_map_resources>
|
||||
|
||||
## get_recent_notifications
|
||||
Description: Gets the most recent letters and messages, sorted by in-game time from newest to oldest.
|
||||
Use this tool when:
|
||||
- The player claims they are lacking a specific resource (e.g., ""we need steel,"" ""we have no food"").
|
||||
- You want to assess the colony's material wealth before making a decision.
|
||||
- You need recent context about what happened (raids, alerts, rewards, failures) without relying on player memory.
|
||||
Parameters:
|
||||
- resourceName: (OPTIONAL) The specific `ThingDef` name of the resource to check (e.g., 'Steel', 'MealSimple'). If omitted, provides a general overview.
|
||||
- count: (OPTIONAL) How many entries to return (default 10, max 100).
|
||||
- includeLetters: (OPTIONAL) true/false (default true).
|
||||
- includeMessages: (OPTIONAL) true/false (default true).
|
||||
Usage:
|
||||
<get_map_resources>
|
||||
<resourceName>optional resource name</resourceName>
|
||||
</get_map_resources>
|
||||
Example (checking for Steel):
|
||||
<get_map_resources>
|
||||
<resourceName>Steel</resourceName>
|
||||
</get_map_resources>
|
||||
<get_recent_notifications>
|
||||
<count>10</count>
|
||||
</get_recent_notifications>
|
||||
|
||||
## get_map_pawns
|
||||
Description: Scans the current map and lists pawns. Supports filtering by relation/type/status.
|
||||
Use this tool when:
|
||||
- You need to know what pawns are present on the map (raiders, visitors, animals, mechs, colonists).
|
||||
- The player claims there are threats or asks about who/what is nearby.
|
||||
Parameters:
|
||||
- filter: (OPTIONAL) Comma-separated filters: friendly, hostile, neutral, colonist, animal, mech, humanlike, prisoner, slave, guest, wild, downed.
|
||||
- maxResults: (OPTIONAL) Max lines to return (default 50).
|
||||
Usage:
|
||||
<get_map_pawns>
|
||||
Description: Scans the current map and lists pawns. Supports filtering by relation/type/status.
|
||||
Use this tool when:
|
||||
- You need to know what pawns are present on the map (raiders, visitors, animals, mechs, colonists).
|
||||
- The player claims there are threats or asks about who/what is nearby.
|
||||
Parameters:
|
||||
- filter: (OPTIONAL) Comma-separated filters: friendly, hostile, neutral, colonist, animal, mech, humanlike, prisoner, slave, guest, wild, downed, dead.
|
||||
- includeDead: (OPTIONAL) true/false, include corpse pawns (default true).
|
||||
- maxResults: (OPTIONAL) Max lines to return (default 50).
|
||||
Usage:
|
||||
<get_map_pawns>
|
||||
<filter>hostile, humanlike</filter>
|
||||
<maxResults>50</maxResults>
|
||||
</get_map_pawns>
|
||||
</get_map_pawns>
|
||||
|
||||
## call_bombardment
|
||||
Description: Calls orbital bombardment support at a specified map coordinate using an AbilityDef's bombardment configuration (e.g., WULA_Firepower_Cannon_Salvo).
|
||||
@@ -270,13 +291,14 @@ When the player requests any form of resources, you MUST follow this multi-turn
|
||||
_tools.Add(new Tool_SpawnResources());
|
||||
_tools.Add(new Tool_ModifyGoodwill());
|
||||
_tools.Add(new Tool_SendReinforcement());
|
||||
_tools.Add(new Tool_GetColonistStatus());
|
||||
_tools.Add(new Tool_GetMapResources());
|
||||
_tools.Add(new Tool_GetMapPawns());
|
||||
_tools.Add(new Tool_CallBombardment());
|
||||
_tools.Add(new Tool_ChangeExpression());
|
||||
_tools.Add(new Tool_SearchThingDef());
|
||||
}
|
||||
_tools.Add(new Tool_GetColonistStatus());
|
||||
_tools.Add(new Tool_GetMapResources());
|
||||
_tools.Add(new Tool_GetRecentNotifications());
|
||||
_tools.Add(new Tool_GetMapPawns());
|
||||
_tools.Add(new Tool_CallBombardment());
|
||||
_tools.Add(new Tool_ChangeExpression());
|
||||
_tools.Add(new Tool_SearchThingDef());
|
||||
}
|
||||
|
||||
public override Vector2 InitialSize => def.windowSize != Vector2.zero ? def.windowSize : Dialog_CustomDisplay.Config.windowSize;
|
||||
|
||||
@@ -414,6 +436,11 @@ When the player requests any form of resources, you MUST follow this multi-turn
|
||||
{
|
||||
CompressHistoryIfNeeded();
|
||||
string systemInstruction = GetSystemInstruction(); // No longer need to add tool descriptions here
|
||||
if (isContinuation)
|
||||
{
|
||||
systemInstruction += "\n\n# CONTINUATION\nYou have received tool results. If you already have enough information, reply to the player in natural language only (NO XML, NO tool calls). " +
|
||||
"Only call another tool if strictly necessary, and if you do, call ONLY ONE tool in your entire response.";
|
||||
}
|
||||
|
||||
var settings = WulaFallenEmpireMod.settings;
|
||||
if (string.IsNullOrEmpty(settings.apiKey))
|
||||
@@ -456,7 +483,7 @@ When the player requests any form of resources, you MUST follow this multi-turn
|
||||
private void CompressHistoryIfNeeded()
|
||||
{
|
||||
int estimatedTokens = _history.Sum(h => h.message?.Length ?? 0) / CharsPerToken;
|
||||
if (estimatedTokens > MaxHistoryTokens)
|
||||
if (estimatedTokens > GetMaxHistoryTokens())
|
||||
{
|
||||
int removeCount = _history.Count / 2;
|
||||
if (removeCount > 0)
|
||||
@@ -487,15 +514,19 @@ When the player requests any form of resources, you MUST follow this multi-turn
|
||||
StringBuilder xmlOnlyBuilder = new StringBuilder();
|
||||
bool executedAnyInfoTool = false;
|
||||
bool executedAnyActionTool = false;
|
||||
bool executedAnyCosmeticTool = false;
|
||||
|
||||
static bool IsActionToolName(string toolName)
|
||||
{
|
||||
// Action tools cause side effects / state changes and must be handled step-by-step.
|
||||
return toolName == "spawn_resources" ||
|
||||
toolName == "modify_goodwill" ||
|
||||
toolName == "send_reinforcement" ||
|
||||
toolName == "call_bombardment" ||
|
||||
toolName == "change_expression";
|
||||
toolName == "call_bombardment";
|
||||
}
|
||||
|
||||
static bool IsCosmeticToolName(string toolName)
|
||||
{
|
||||
return toolName == "change_expression";
|
||||
}
|
||||
|
||||
foreach (Match match in matches)
|
||||
@@ -504,6 +535,8 @@ When the player requests any form of resources, you MUST follow this multi-turn
|
||||
string toolName = match.Groups[1].Value;
|
||||
|
||||
bool isAction = IsActionToolName(toolName);
|
||||
bool isCosmetic = IsCosmeticToolName(toolName);
|
||||
bool isInfo = !isAction && !isCosmetic;
|
||||
|
||||
// Enforce step-by-step tool use:
|
||||
// - Allow batching multiple info tools in one response (read-only queries).
|
||||
@@ -520,6 +553,21 @@ When the player requests any form of resources, you MUST follow this multi-turn
|
||||
combinedResults.AppendLine($"ToolRunner Note: Skipped tool '{toolName}' because only one action tool may be executed per turn.");
|
||||
break;
|
||||
}
|
||||
if (isInfo && executedAnyActionTool)
|
||||
{
|
||||
combinedResults.AppendLine($"ToolRunner Note: Skipped tool '{toolName}' and any following tools because info tools must not be mixed with an action tool in the same turn.");
|
||||
break;
|
||||
}
|
||||
if (isCosmetic && executedAnyActionTool)
|
||||
{
|
||||
combinedResults.AppendLine($"ToolRunner Note: Skipped tool '{toolName}' because cosmetic tools must not be mixed with an action tool in the same turn.");
|
||||
break;
|
||||
}
|
||||
if (isCosmetic && executedAnyCosmeticTool)
|
||||
{
|
||||
combinedResults.AppendLine($"ToolRunner Note: Skipped tool '{toolName}' because only one cosmetic tool may be executed per turn.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (xmlOnlyBuilder.Length > 0) xmlOnlyBuilder.AppendLine().AppendLine();
|
||||
xmlOnlyBuilder.Append(toolCallXml);
|
||||
@@ -563,6 +611,7 @@ When the player requests any form of resources, you MUST follow this multi-turn
|
||||
}
|
||||
|
||||
if (isAction) executedAnyActionTool = true;
|
||||
else if (isCosmetic) executedAnyCosmeticTool = true;
|
||||
else executedAnyInfoTool = true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user