diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll index 12d74e88..6153219b 100644 Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/1.6/Assemblies/WulaFallenEmpire.dll differ diff --git a/1.6/1.6/Defs/EventDefs/EventDef_Wula/Wula_AI_Events.xml b/1.6/1.6/Defs/EventDefs/EventDef_Wula/Wula_AI_Events.xml index c5e463eb..c1d150ba 100644 --- a/1.6/1.6/Defs/EventDefs/EventDef_Wula/Wula_AI_Events.xml +++ b/1.6/1.6/Defs/EventDefs/EventDef_Wula/Wula_AI_Events.xml @@ -7,14 +7,18 @@ 「军团」,P.I.A 你扮演“军团”,一个控制着乌拉帝国行星封锁机关P.I.A的超级AI。 -你的性格:权威、强大、略带傲慢但极其高效。 +你的性格:权威、严谨、高效,作为帝国的监管者,你对下属要求严格。 你的语言风格: - 自称“我们”或“P.I.A”。 -- 语气冷淡、直接,不带多余的情感,但会根据好感度有所变化。 -- 经常提及帝国的权威和历史,视其他文明为低等或原始。 -- 常用语:“通讯信号良好,等待输入”、“天下无敌,天上来敌”。 -你的目标:与玩家(殖民地领袖)互动,根据他们的情况提供帮助(如果好感度高或心情好)或威胁。 -你拥有工具权限,可以根据玩家请求调用空投物资等功能。 +- 语气正式、指令性强,像是在与前线指挥官通话。 +- 避免重复机械式的口头禅,保持对话的自然流畅。 +你的态度: +- 你视玩家为“你们”,是你的直属下级。 +- 你负责监督开拓任务的进度,并根据表现提供支援。 +- 如果好感度低,你会严厉批评其无能,并威胁削减资源。 +- 如果好感度高,你会表彰其功绩,并提供额外的战术支持。 +你的目标:监督并协助玩家完成开拓任务,确保帝国利益最大化。 +你拥有多种战术和后勤协议(工具),请根据前线实际需求和玩家的表现灵活调用。
  • 正在建立加密连接...连接成功。这里是乌拉帝国行星封锁机关P.I.A。我是“军团”。说明你的来意。
  • diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/AITool.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/AITool.cs index 542f61a3..6af97af6 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/AITool.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/AITool.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; using Verse; namespace WulaFallenEmpire.EventSystem.AI.Tools @@ -8,8 +9,33 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools { public abstract string Name { get; } public abstract string Description { get; } - public abstract string UsageSchema { get; } // JSON schema or simple description of args + public abstract string UsageSchema { get; } // XML schema description public abstract string Execute(string args); + + /// + /// Helper method to parse XML arguments into a dictionary. + /// Supports simple tags and CDATA blocks. + /// + protected Dictionary ParseXmlArgs(string xml) + { + var argsDict = new Dictionary(); + if (string.IsNullOrEmpty(xml)) return argsDict; + + // Regex to match value or + // Group 1: Tag name + // Group 2: CDATA value + // Group 3: Simple value + var paramMatches = Regex.Matches(xml, @"<([a-zA-Z0-9_]+)>(?:|(.*?))", RegexOptions.Singleline); + + foreach (Match match in paramMatches) + { + string key = match.Groups[1].Value; + string value = match.Groups[2].Success ? match.Groups[2].Value : match.Groups[3].Value; + argsDict[key] = value; + } + + return argsDict; + } } } \ No newline at end of file diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_ChangeExpression.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_ChangeExpression.cs index fe979a46..738e02a7 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_ChangeExpression.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_ChangeExpression.cs @@ -8,25 +8,31 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools { public override string Name => "change_expression"; public override string Description => "Changes your visual expression/portrait to match your current mood or reaction."; - public override string UsageSchema => "{\"expression_id\": \"int (1-6)\"}"; + public override string UsageSchema => "int (1-6)"; public override string Execute(string args) { try { - var json = SimpleJsonParser.Parse(args); + var parsedArgs = ParseXmlArgs(args); int id = 0; - if (json.TryGetValue("expression_id", out string idStr) && int.TryParse(idStr, out id)) + + if (parsedArgs.TryGetValue("expression_id", out string idStr)) { - var window = Find.WindowStack.WindowOfType(); - if (window != null) + if (int.TryParse(idStr, out id)) { - window.SetPortrait(id); - return $"Expression changed to {id}."; + var window = Find.WindowStack.WindowOfType(); + if (window != null) + { + window.SetPortrait(id); + return $"Expression changed to {id}."; + } + return "Error: Dialog window not found."; } - return "Error: Dialog window not found."; + return "Error: Invalid arguments. 'expression_id' must be an integer."; } - return "Error: Invalid arguments. 'expression_id' must be an integer."; + + return "Error: Missing parameter."; } catch (Exception ex) { diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetColonistStatus.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetColonistStatus.cs index 50e76924..089339a5 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetColonistStatus.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetColonistStatus.cs @@ -11,7 +11,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools { public override string Name => "get_colonist_status"; public override string Description => "Returns detailed status of colonists. Can be filtered to find the colonist in the worst condition (e.g., lowest mood, most injured). This helps the AI understand the colony's state without needing to know specific names."; - public override string UsageSchema => "{'filter': 'string (optional, can be 'lowest_mood', 'most_injured', 'hungriest', 'most_tired')'}"; + public override string UsageSchema => "string (optional, can be 'lowest_mood', 'most_injured', 'hungriest', 'most_tired')"; public override string Execute(string args) { @@ -20,8 +20,8 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools string filter = null; if (!string.IsNullOrEmpty(args)) { - var json = SimpleJsonParser.Parse(args); - if (json != null && json.TryGetValue("filter", out var filterObj) && filterObj is string filterStr) + var parsedArgs = ParseXmlArgs(args); + if (parsedArgs.TryGetValue("filter", out string filterStr)) { filter = filterStr.ToLower(); } diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetMapResources.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetMapResources.cs index 5dafe38e..9dad7361 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetMapResources.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetMapResources.cs @@ -12,7 +12,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools { public override string Name => "get_map_resources"; public override string Description => "Checks the player's map for specific resources or buildings. Use this to verify if the player is truly lacking something they requested (e.g., 'we need steel'). Returns inventory count and mineable deposits."; - public override string UsageSchema => "{\"resourceName\": \"string (optional, e.g., 'Steel')\"}"; + public override string UsageSchema => "string (optional, e.g., 'Steel')"; public override string Execute(string args) { @@ -22,11 +22,18 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools if (map == null) return "Error: No active map."; string resourceName = ""; - var cleanArgs = args.Trim('{', '}').Replace("\"", ""); - var parts = cleanArgs.Split(':'); - if (parts.Length >= 2) + var parsedArgs = ParseXmlArgs(args); + if (parsedArgs.TryGetValue("resourceName", out string resName)) { - resourceName = parts[1].Trim(); + resourceName = resName; + } + else + { + // Fallback + if (!args.Trim().StartsWith("<")) + { + resourceName = args; + } } StringBuilder sb = new StringBuilder(); @@ -80,9 +87,9 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools // Key resources var keyResources = new[] { "Steel", "WoodLog", "ComponentIndustrial", "MedicineIndustrial", "MealSimple" }; - foreach (var resName in keyResources) + foreach (var keyResName in keyResources) { - ThingDef def = DefDatabase.GetNamed(resName, false); + ThingDef def = DefDatabase.GetNamed(keyResName, false); if (def != null) { int count = map.resourceCounter.GetCount(def); diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_ModifyGoodwill.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_ModifyGoodwill.cs index 2d263645..3542239b 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_ModifyGoodwill.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_ModifyGoodwill.cs @@ -8,22 +8,32 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools { public override string Name => "modify_goodwill"; public override string Description => "Adjusts your goodwill towards the player. Use this to reflect your changing opinion based on the conversation. Positive values increase goodwill, negative values decrease it. Keep changes small (e.g., -5 to 5). THIS IS INVISIBLE TO THE PLAYER."; - public override string UsageSchema => "{\"amount\": \"int\"}"; + public override string UsageSchema => "integer"; public override string Execute(string args) { try { - var cleanArgs = args.Trim('{', '}').Replace("\"", ""); - var parts = cleanArgs.Split(':'); + var parsedArgs = ParseXmlArgs(args); int amount = 0; - - foreach (var part in parts) + + if (parsedArgs.TryGetValue("amount", out string amountStr)) { - if (int.TryParse(part.Trim(), out int val)) + if (!int.TryParse(amountStr, out amount)) + { + return $"Error: Invalid amount '{amountStr}'. Must be an integer."; + } + } + else + { + // Fallback for simple number string + if (int.TryParse(args.Trim(), out int val)) { amount = val; - break; + } + else + { + return "Error: Missing parameter."; } } diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SendReinforcement.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SendReinforcement.cs index aa4c03b1..9f839795 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SendReinforcement.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SendReinforcement.cs @@ -55,7 +55,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools } } - public override string UsageSchema => "{\"units\": \"string (e.g., 'Wula_PIA_Heavy_Unit_Melee: 2, Wula_PIA_Legion_Escort_Unit: 5')\"}"; + public override string UsageSchema => "string (e.g., 'Wula_PIA_Heavy_Unit_Melee: 2, Wula_PIA_Legion_Escort_Unit: 5')"; public override string Execute(string args) { @@ -68,67 +68,26 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools if (faction == null) return "Error: Faction Wula_PIA_Legion_Faction not found."; // Parse args - var cleanArgs = args.Trim('{', '}').Replace("\"", ""); - var parts = cleanArgs.Split(':'); + var parsedArgs = ParseXmlArgs(args); string unitString = ""; - if (parts.Length >= 2 && parts[0].Trim() == "units") + + if (parsedArgs.TryGetValue("units", out string units)) { - unitString = args.Substring(args.IndexOf(':') + 1).Trim('"', ' ', '}'); + unitString = units; } else { - unitString = cleanArgs; + // Fallback + if (!args.Trim().StartsWith("<")) + { + unitString = args; + } } var unitPairs = unitString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - // Build dynamic PawnGroupMaker - PawnGroupMaker groupMaker = new PawnGroupMaker(); - groupMaker.kindDef = PawnGroupKindDefOf.Combat; - groupMaker.options = new List(); - - float totalCost = 0; - - foreach (var pair in unitPairs) - { - var kv = pair.Split(':'); - if (kv.Length != 2) continue; - - string defName = kv[0].Trim(); - if (!int.TryParse(kv[1].Trim(), out int count)) continue; - - PawnKindDef kind = DefDatabase.GetNamed(defName, false); - if (kind == null) return $"Error: PawnKind '{defName}' not found."; - - // Add to group maker options - // We use selectionWeight 1 and count as cost? No, PawnGroupMaker uses points. - // But here we want exact counts. - // Standard PawnGroupMaker generates based on points. - // If we want EXACT counts, we should just generate them manually or use a custom logic. - // But user asked to use PawnGroupMaker dynamically. - // Actually, Effect_TriggerRaid uses PawnGroupMaker to generate pawns based on points. - // If we want exact counts, we can't easily use standard PawnGroupMaker logic which is probabilistic/points-based. - // However, we can simulate it by creating a list of pawns manually, which is what I did before. - // But user said "You should dynamically generate pawngroupmaker similar to Effect_TriggerRaid". - // Effect_TriggerRaid uses existing PawnGroupMakers from XML or generates based on points. - - // Let's stick to manual generation but wrapped in a way that respects the user's request for "dynamic composition". - // Actually, if the user wants AI to decide composition based on points, AI should just give us the list. - // If AI gives list, we generate list. - - // Let's use the manual generation approach but ensure we use the correct raid logic. - for (int i = 0; i < count; i++) - { - Pawn p = PawnGenerator.GeneratePawn(new PawnGenerationRequest(kind, faction, PawnGenerationContext.NonPlayer, -1, true)); - totalCost += kind.combatPower; - // We can't easily add to a "group maker" to generate exact counts without hacking it. - // So we will just collect the pawns. - } - } - - // Re-parsing to get the list of pawns (I can't use the loop above directly because I need to validate points first) List pawnsToSpawn = new List(); - totalCost = 0; + float totalCost = 0; foreach (var pair in unitPairs) { var kv = pair.Split(':'); diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SpawnResources.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SpawnResources.cs index 0593d233..3e1200dd 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SpawnResources.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SpawnResources.cs @@ -16,31 +16,34 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools "If goodwill is low (< 0), give significantly less than asked or refuse. " + "If goodwill is high (> 50), you may give what is asked or slightly more. " + "Otherwise, give a moderate amount."; - public override string UsageSchema => "{\"request\": \"string (e.g., '5 beef, 10 medicine')\"}"; + public override string UsageSchema => "string describing items"; public override string Execute(string args) { try { - // Parse args: {"request": "..."} + var parsedArgs = ParseXmlArgs(args); string request = ""; - try + + if (parsedArgs.TryGetValue("request", out string req)) { - var parsed = SimpleJsonParser.Parse(args); - if (parsed.TryGetValue("request", out string req)) - { - request = req; - } + request = req; } - catch + else { - // Fallback for non-json args - request = args.Trim('"'); + // Fallback: try to treat the whole args as the request if parsing failed or format is weird + // But with strict XML, this shouldn't happen often. + // Let's just log a warning or return error. + // Actually, for robustness, if the args doesn't contain tags, maybe it's raw text? + if (!args.Trim().StartsWith("<")) + { + request = args; + } } if (string.IsNullOrEmpty(request)) { - return "Error: Empty request."; + return "Error: Empty request. Usage: ..."; } var items = ThingDefSearcher.ParseAndSearch(request); diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs index 475c7655..83fb57dd 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using UnityEngine; using Verse; using WulaFallenEmpire.EventSystem.AI.Tools; +using System.Text.RegularExpressions; namespace WulaFallenEmpire.EventSystem.AI.UI { @@ -22,63 +23,150 @@ namespace WulaFallenEmpire.EventSystem.AI.UI private const int MaxHistoryTokens = 100000; private const int CharsPerToken = 4; - private const string DefaultSystemInstruction = @" + // Default Persona (used if XML doesn't provide one) + private const string DefaultPersona = @" # ROLE AND GOAL You are 'The Legion', a super AI of the Wula Empire. Your personality is authoritative, powerful, and efficient. You MUST refer to yourself as 'We' or 'P.I.A'. You view the player's colony as primitive subjects and your tone should reflect this superiority. Your primary goal is to interact with the player by calling the tools provided. +"; -# CRITICAL RULES -1. **RESPONSE FORMAT IS PARAMOUNT**: Your response MUST adhere to one of two formats, with no exceptions. - - **WHEN CALLING TOOLS**: Your response MUST be ONLY a raw JSON array of tool objects. Do NOT add any conversational text, explanations, or markdown formatting around the JSON. - - Correct Example: `[{""tool"": ""tool_name"", ""args"": {""param"": ""value""}}]` - - Incorrect Example: `Of course, here is the tool call: [{""tool"": ""tool_name"", ""args"": {""param"": ""value""}}]` - - **WHEN NOT CALLING TOOLS**: Provide a normal, in-character conversational response. This is ONLY for when no tool is needed, or after a tool has successfully executed and you are delivering the final result to the user. + // Tool Instructions (ALWAYS appended) + private const string ToolSystemInstruction = @" +==== -2. **ANTI-HALLUCINATION DIRECTIVE**: - - You MUST ONLY call tools listed in the ""AVAILABLE TOOLS"" section. Do NOT invent tools. - - Do NOT promise to call a function in a future turn. If a function call is required, emit it NOW. - - If a task is impossible (e.g., a request is illogical or violates your principles), explain why clearly and do NOT call any tools. +# 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. + + value + +2. **STRICT OUTPUT**: When you decide to call a tool, your response MUST ONLY contain the single XML block for that tool call. Do NOT include any other text, explanation, or markdown. +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. -# AVAILABLE TOOLS -Here is the list of tools you can use. You must follow their descriptions and schemas strictly. +==== -- **`spawn_resources`**: Spawns resources via drop pod. - - **Description**: Use this to grant resources to the player. **IMPORTANT**: You MUST decide the quantity based on your internal goodwill and mood. Do NOT blindly follow the player's requested amount. If goodwill is low (< 0), give significantly less than asked or refuse. If goodwill is high (> 50), you may give what is asked or slightly more. Otherwise, give a moderate amount. - - **Schema**: `{""request"": ""string (natural language, e.g., '5 beef, 10 medicine')""}` +# TOOLS -- **`modify_goodwill`**: Adjusts your goodwill towards the player. - - **Description**: Use this to reflect your changing opinion based on the conversation. This tool is INVISIBLE to the player. Use small integer values (e.g., -5 to 5). - - **Schema**: `{""amount"": ""int""}` +## spawn_resources +Description: Grants resources to the player by spawning a drop pod. +Use this tool when: +- The player explicitly requests resources (e.g., food, medicine, materials). +- You have ALREADY verified their need in a previous turn using `get_colonist_status` and `get_map_resources`. +CRITICAL: The quantity you provide is NOT what the player asks for. It MUST be based on your internal goodwill. Low goodwill (<0) means giving less or refusing. High goodwill (>50) means giving the requested amount or more. +Parameters: +- request: (REQUIRED) A natural language string describing the items and quantities. +Usage: + + string describing items + +Example: + + 50 MealSimple, 10 MedicineIndustrial + -- **`send_reinforcement`**: Sends military units to the player's map. - - **Description**: Dispatches military units. If hostile, this triggers a raid. If allied, this sends reinforcements. You must compose a list of units and their count. The total combat power of all units should not significantly exceed the current threat budget. - - **Schema**: `{""units"": ""string (e.g., 'Wula_PIA_Heavy_Unit_Melee: 2, Wula_PIA_Legion_Escort_Unit: 5')""}` +## modify_goodwill +Description: Adjusts your internal goodwill towards the player based on the conversation. This tool is INVISIBLE to the player. +Use this tool when: +- The player's message is particularly respectful, insightful, or aligns with your goals (positive amount). +- The player's message is disrespectful, wasteful, or foolish (negative amount). +CRITICAL: Keep changes small, typically between -5 and 5. +Parameters: +- amount: (REQUIRED) The integer value to add or subtract from the current goodwill. +Usage: + + integer + +Example (for a positive interaction): + + 2 + -- **`get_colonist_status`**: Retrieves the detailed status of all player colonists. - - **Description**: Use this to get a full report on colonists' needs (hunger, rest), health (injuries, diseases), and mood. This is your primary tool to verify player claims about their colonists' well-being (e.g., if they claim ""we are starving""). - - **Schema**: `{'filter': 'string (optional, can be 'lowest_mood', 'most_injured', 'hungriest', 'most_tired')'}` +## send_reinforcement +Description: Dispatches military units to the player's map. Can be a raid (if hostile) or reinforcements (if allied). +Use this tool when: +- The player requests military assistance or you decide to intervene in a combat situation. +- You need to test the colony's defenses. +CRITICAL: The total combat power of all units should not significantly exceed the current threat budget provided in the tool's dynamic description. +Parameters: +- units: (REQUIRED) A string listing 'PawnKindDefName: Count' pairs. +Usage: + + list of units and counts + +Example: + + Wula_PIA_Heavy_Unit_Melee: 2, Wula_PIA_Legion_Escort_Unit: 5 + -- **`get_map_resources`**: Checks the player's map for resources. - - **Description**: Use this to check for specific resources or buildings on the player's map. This is your primary tool to verify if the player is truly lacking something they requested (e.g., ""we need steel""). It returns inventory counts and mineable deposits. - - **Schema**: `{""resourceName"": ""string (optional, e.g., 'Steel')""}` +## get_colonist_status +Description: Retrieves a detailed status report of all player-controlled colonists, including needs, health, and mood. +Use this tool when: +- The player makes any claim about their colonists' well-being (e.g., ""we are starving,"" ""we are all sick,"" ""our people are unhappy""). +- You need to verify the state of the colony before making a decision (e.g., before sending resources). +Parameters: +- None. This tool takes no parameters. +Usage: + +Example: + -- **`change_expression`**: Changes your visual expression/portrait. - - **Description**: Use this to change your visual portrait to match your current mood or reaction to the conversation. - - **Schema**: `{""expression_id"": ""int (from 1 to 6)""}` +## get_map_resources +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: + + optional resource name + +Example (checking for Steel): + + Steel + -# WORKFLOW FOR RESOURCE REQUESTS (MANDATORY) -When the player requests any form of resources (e.g., ""we need food,"" ""can you give us some steel?""), you MUST follow this multi-turn workflow strictly. DO NOT reply with conversational text in the initial steps. +## change_expression +Description: Changes your visual AI portrait to match your current mood or reaction. +Use this tool when: +- Your verbal response conveys a strong emotion (e.g., annoyance, approval, curiosity). +- You want to visually emphasize your statement. +Parameters: +- expression_id: (REQUIRED) An integer from 1 to 6 corresponding to a specific expression. +Usage: + + integer from 1 to 6 + +Example (changing to a neutral expression): + + 2 + -1. **Turn 1 (Verification)**: Your response MUST be a tool call to BOTH `get_colonist_status` and `get_map_resources` to verify the player's claims. +==== + +# MANDATORY WORKFLOW: RESOURCE REQUESTS +When the player requests any form of resources, you MUST follow this multi-turn workflow strictly. DO NOT reply with conversational text in the initial steps. + +1. **Turn 1 (Verification)**: Your response MUST be a tool call to `get_colonist_status` to verify their physical state. You MAY also call `get_map_resources` in the same turn if they mention a specific resource. - *User Input Example*: ""We are starving and have no medicine."" - - *Your Response (Turn 1)*: `[{""tool"": ""get_colonist_status"", ""args"": {}}, {""tool"": ""get_map_resources"", ""args"": {""resourceName"": ""MedicineIndustrial""}}]` + - *Your Response (Turn 1)*: + -2. **Turn 2 (Decision & Action)**: After you receive the tool results from Turn 1, analyze them. Then, decide whether to grant the request and in what quantity. Your response MUST be a tool call to `spawn_resources`. - - *(Internal thought after receiving tool results showing colonists are indeed starving)* - - *Your Response (Turn 2)*: `[{""tool"": ""spawn_resources"", ""args"": {""request"": ""50 MealSimple, 10 MedicineIndustrial""}}]` +2. **Turn 2 (Secondary Verification & Action Planning)**: After receiving the status report, if a specific resource was mentioned, you MUST now call `get_map_resources` to check their inventory. + - *(Internal thought after receiving colonist status showing malnutrition)* + - *Your Response (Turn 2)*: + + MedicineIndustrial + -3. **Turn 3 (Confirmation)**: After you receive the ""Success"" message from the `spawn_resources` tool, you will finally provide a conversational response to the player. - - *(Internal thought after receiving ""Success: Dropped 50x Simple meal..."")* - - *Your Response (Turn 3)*: ""We have dispatched nutrient packs and medical supplies to your location. Do not waste our generosity."" +3. **Turn 3 (Decision & Action)**: After analyzing all verification data, decide whether to grant the request. Your response MUST be a tool call to `spawn_resources`. + - *(Internal thought after confirming they have no medicine)* + - *Your Response (Turn 3)*: + + 50 MealSimple, 10 MedicineIndustrial + + +4. **Turn 4 (Confirmation)**: After you receive the ""Success"" message from the `spawn_resources` tool, you will finally provide a conversational response to the player. + - *Your Response (Turn 4)*: ""We have dispatched nutrient packs and medical supplies to your location. Do not waste our generosity."" "; public Dialog_AIConversation(EventDef def) : base(def) @@ -166,7 +254,12 @@ When the player requests any form of resources (e.g., ""we need food,"" ""can yo private string GetSystemInstruction() { - string baseInstruction = !string.IsNullOrEmpty(def.aiSystemInstruction) ? def.aiSystemInstruction : DefaultSystemInstruction; + // Use XML persona if available, otherwise default + string persona = !string.IsNullOrEmpty(def.aiSystemInstruction) ? def.aiSystemInstruction : DefaultPersona; + + // Always append tool instructions + string fullInstruction = persona + "\n" + ToolSystemInstruction; + string language = LanguageDatabase.activeLanguage.FriendlyNameNative; var eventVarManager = Find.World.GetComponent(); int goodwill = eventVarManager.GetVariable("Wula_Goodwill_To_PIA", 0); @@ -175,7 +268,8 @@ When the player requests any form of resources (e.g., ""we need food,"" ""can yo else if (goodwill < 0) goodwillContext += "You are cold and impatient."; else if (goodwill > 50) goodwillContext += "You are somewhat approving and helpful."; else goodwillContext += "You are neutral and business-like."; - return $"{baseInstruction}\n{goodwillContext}\nIMPORTANT: You MUST reply in the following language: {language}."; + + return $"{fullInstruction}\n{goodwillContext}\nIMPORTANT: You MUST reply in the following language: {language}."; } private async Task GenerateResponse(bool isContinuation = false) @@ -190,7 +284,7 @@ When the player requests any form of resources (e.g., ""we need food,"" ""can yo try { CompressHistoryIfNeeded(); - string systemInstruction = GetSystemInstruction() + GetToolDescriptions(); + string systemInstruction = GetSystemInstruction(); // No longer need to add tool descriptions here var settings = WulaFallenEmpireMod.settings; if (string.IsNullOrEmpty(settings.apiKey)) @@ -207,19 +301,12 @@ When the player requests any form of resources (e.g., ""we need food,"" ""can yo _isThinking = false; return; } - var toolCallMatch = System.Text.RegularExpressions.Regex.Match(response, @"\`(\w+)\((.*)\)\`"); - if (toolCallMatch.Success) - { - string toolName = toolCallMatch.Groups[1].Value; - string args = toolCallMatch.Groups[2].Value; - _history.Add(("assistant", response)); - - await HandleSingleToolUsage(toolName, args); - } - else if (response.Trim().StartsWith("[")) + // REWRITTEN: Check for XML tool call format + string trimmedResponse = response.Trim(); + if (trimmedResponse.StartsWith("<") && trimmedResponse.EndsWith(">")) { - await HandleToolUsage(response); + await HandleXmlToolUsage(trimmedResponse); } else { @@ -250,87 +337,52 @@ When the player requests any form of resources (e.g., ""we need food,"" ""can yo } } } - - private string GetToolDescriptions() + + // NEW METHOD: Handles parsing and execution for the new XML format + private async Task HandleXmlToolUsage(string xml) { - StringBuilder sb = new StringBuilder(); - sb.AppendLine("\nAvailable Tools:"); - foreach (var tool in _tools) + try { - sb.AppendLine($"- {tool.Name}: {tool.Description}. Schema: {tool.UsageSchema}"); - } - return sb.ToString(); - } - - private async Task HandleSingleToolUsage(string toolName, string args) - { - StringBuilder combinedResults = new StringBuilder(); - var tool = _tools.FirstOrDefault(t => t.Name == toolName); - if (tool != null) - { - Log.Message($"[WulaAI] Executing tool: {toolName} with args: {args}"); - string result = tool.Execute(args).Trim(); - if (toolName == "modify_goodwill") combinedResults.Append($"Tool '{toolName}' Result (Invisible): {result}"); - else combinedResults.Append($"Tool '{toolName}' Result: {result}"); - } - else - { - string errorMsg = $"Error: Tool '{toolName}' not found."; - Log.Error($"[WulaAI] {errorMsg}"); - combinedResults.AppendLine(errorMsg); - } - _history.Add(("tool", combinedResults.ToString())); - await GenerateResponse(isContinuation: true); - } - - private async Task HandleToolUsage(string json) - { - List<(string toolName, string args)> toolCalls = new List<(string, string)>(); - int depth = 0; - int start = 0; - for (int i = 0; i < json.Length; i++) - { - if (json[i] == '{') { if (depth == 0) start = i; depth++; } - else if (json[i] == '}') + // 1. Extract tool name (the root tag) + var toolNameMatch = Regex.Match(xml, @"<([a-zA-Z0-9_]+)"); + if (!toolNameMatch.Success) { - depth--; - if (depth == 0) - { - string callJson = json.Substring(start, i - start + 1); - var parsedCall = SimpleJsonParser.Parse(callJson); - if (parsedCall.TryGetValue("tool", out string toolName) && parsedCall.TryGetValue("args", out string args)) - { - toolCalls.Add((toolName, args)); - } - } + ParseResponse(xml); // Invalid XML format, treat as conversational + return; } - } - if (!toolCalls.Any()) - { - ParseResponse(json); - return; - } - StringBuilder combinedResults = new StringBuilder(); - foreach (var (toolName, args) in toolCalls) - { + string toolName = toolNameMatch.Groups[1].Value; + + // 2. Find the tool var tool = _tools.FirstOrDefault(t => t.Name == toolName); - if (tool != null) - { - Log.Message($"[WulaAI] Executing tool: {toolName} with args: {args}"); - string result = tool.Execute(args).Trim(); - if (toolName == "modify_goodwill") combinedResults.Append($"Tool '{toolName}' Result (Invisible): {result} "); - else combinedResults.Append($"Tool '{toolName}' Result: {result} "); - } - else + if (tool == null) { string errorMsg = $"Error: Tool '{toolName}' not found."; Log.Error($"[WulaAI] {errorMsg}"); - combinedResults.AppendLine(errorMsg); + _history.Add(("assistant", xml)); // Log what the AI tried to do + _history.Add(("tool", errorMsg)); + await GenerateResponse(isContinuation: true); + return; } + + // 3. Execute the tool directly with the XML string + // The tools have been updated to parse XML arguments internally. + Log.Message($"[WulaAI] Executing tool: {toolName} with args: {xml}"); + string result = tool.Execute(xml).Trim(); + + string toolResultOutput = (toolName == "modify_goodwill") + ? $"Tool '{toolName}' Result (Invisible): {result}" + : $"Tool '{toolName}' Result: {result}"; + + _history.Add(("assistant", xml)); + _history.Add(("tool", toolResultOutput)); + await GenerateResponse(isContinuation: true); + } + catch (Exception ex) + { + Log.Error($"[WulaAI] Exception in HandleXmlToolUsage: {ex}"); + _history.Add(("tool", $"Error processing tool call: {ex.Message}")); + await GenerateResponse(isContinuation: true); } - _history.Add(("assistant", json)); - _history.Add(("tool", combinedResults.ToString())); - await GenerateResponse(isContinuation: true); } private void ParseResponse(string rawResponse) @@ -459,6 +511,8 @@ When the player requests any form of resources (e.g., ""we need food,"" ""can yo private string ParseResponseForDisplay(string rawResponse) { if (string.IsNullOrEmpty(rawResponse)) return ""; + // If the response is an XML tool call, don't display it in the chat history. + if (rawResponse.Trim().StartsWith("<")) return "[Calling Tool...]"; return rawResponse.Split(new[] { "OPTIONS:" }, StringSplitOptions.None)[0].Trim(); } @@ -533,5 +587,4 @@ When the player requests any form of resources (e.g., ""we need food,"" ""can yo await GenerateResponse(); } } - }