新增原生工具调用数据结构与解析:SimpleAIClient.cs
AITool 增加 Schema 构造器与函数定义生成,所有工具补齐 GetParametersSchema():AITool.cs 与 *.cs
This commit is contained in:
@@ -690,6 +690,32 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
$"Final replies are generated later and MUST use: {language}.";
|
||||
}
|
||||
|
||||
private string GetNativeSystemInstruction()
|
||||
{
|
||||
string persona = GetActivePersona();
|
||||
string memoryContext = GetMemoryContext();
|
||||
string personaBlock = string.IsNullOrWhiteSpace(memoryContext) ? persona : (persona + memoryContext);
|
||||
|
||||
string language = LanguageDatabase.activeLanguage?.FriendlyNameNative ?? "English";
|
||||
var eventVarManager = Find.World?.GetComponent<EventVariableManager>();
|
||||
int goodwill = eventVarManager?.GetVariable<int>("Wula_Goodwill_To_PIA", 0) ?? 0;
|
||||
string goodwillContext = $"Current Goodwill with P.I.A: {goodwill}. ";
|
||||
if (goodwill < -50) goodwillContext += "You are hostile and dismissive towards the player.";
|
||||
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.";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(personaBlock);
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(goodwillContext);
|
||||
sb.AppendLine($"IMPORTANT: Reply in the following language: {language}.");
|
||||
sb.AppendLine("IMPORTANT: Use tools to fetch in-game data or perform actions. Do NOT invent tool results.");
|
||||
sb.AppendLine("IMPORTANT: When the user asks for an item by name, call search_thing_def to confirm the exact defName before spawning.");
|
||||
sb.AppendLine("You MAY include [EXPR:n] (n=1-6) to set your expression.");
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
public string GetActivePersona()
|
||||
{
|
||||
var settings = WulaFallenEmpireMod.settings;
|
||||
@@ -822,6 +848,25 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
private List<Dictionary<string, object>> BuildNativeToolDefinitions()
|
||||
{
|
||||
var available = _tools
|
||||
.Where(t => t != null)
|
||||
.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var definitions = new List<Dictionary<string, object>>();
|
||||
foreach (var tool in available)
|
||||
{
|
||||
var def = tool.GetFunctionDefinition();
|
||||
if (def != null)
|
||||
{
|
||||
definitions.Add(def);
|
||||
}
|
||||
}
|
||||
return definitions;
|
||||
}
|
||||
|
||||
private string GetReactSystemInstruction(bool hasImage)
|
||||
{
|
||||
string persona = GetActivePersona();
|
||||
@@ -974,7 +1019,10 @@ 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)) return true;
|
||||
if (string.Equals(toolName, "capture_screen", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return WulaFallenEmpireMod.settings?.enableVlmFeatures == true;
|
||||
}
|
||||
return _tools.Any(t => string.Equals(t?.Name, toolName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
@@ -1202,6 +1250,50 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private List<ChatMessage> BuildNativeHistory()
|
||||
{
|
||||
var messages = new List<ChatMessage>();
|
||||
if (_history == null || _history.Count == 0) return messages;
|
||||
|
||||
foreach (var entry in _history)
|
||||
{
|
||||
if (entry.role == null) continue;
|
||||
string role = entry.role.Trim().ToLowerInvariant();
|
||||
|
||||
if (role == "toolcall" || role == "tool" || role == "trace")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (role == "assistant")
|
||||
{
|
||||
string cleaned = CleanAssistantForReply(entry.message);
|
||||
if (string.IsNullOrWhiteSpace(cleaned))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
messages.Add(ChatMessage.Assistant(cleaned));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (role == "system")
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(entry.message))
|
||||
{
|
||||
messages.Add(new ChatMessage { Role = "system", Content = entry.message });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(entry.message))
|
||||
{
|
||||
messages.Add(ChatMessage.User(entry.message));
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
private void CompressHistoryIfNeeded()
|
||||
{
|
||||
int estimatedTokens = _history.Sum(h => h.message?.Length ?? 0) / CharsPerToken;
|
||||
@@ -1839,19 +1931,31 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
CompressHistoryIfNeeded();
|
||||
|
||||
var settings = WulaFallenEmpireMod.settings;
|
||||
if (settings == null || string.IsNullOrEmpty(settings.apiKey))
|
||||
if (settings == null)
|
||||
{
|
||||
AddAssistantMessage("Error: API settings not configured in Mod Settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
string apiKey = settings.useGeminiProtocol ? settings.geminiApiKey : settings.apiKey;
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
AddAssistantMessage("Error: API Key not configured in Mod Settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
string apiKey = settings.useGeminiProtocol ? settings.geminiApiKey : settings.apiKey;
|
||||
string baseUrl = settings.useGeminiProtocol ? settings.geminiBaseUrl : settings.baseUrl;
|
||||
string model = settings.useGeminiProtocol ? settings.geminiModel : settings.model;
|
||||
|
||||
var client = new SimpleAIClient(apiKey, baseUrl, model, settings.useGeminiProtocol);
|
||||
_currentClient = client;
|
||||
|
||||
if (!settings.useGeminiProtocol)
|
||||
{
|
||||
await RunNativeToolLoopAsync(client, settings);
|
||||
return;
|
||||
}
|
||||
|
||||
// Model-Driven Vision: Start with null image. The model must request it using analyze_screen or capture_screen if needed.
|
||||
string base64Image = null;
|
||||
float startTime = Time.realtimeSinceStartup;
|
||||
@@ -2135,6 +2239,240 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
SetThinkingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunNativeToolLoopAsync(SimpleAIClient client, WulaFallenEmpireSettings settings)
|
||||
{
|
||||
string systemInstruction = GetNativeSystemInstruction();
|
||||
var messages = BuildNativeHistory();
|
||||
var tools = BuildNativeToolDefinitions();
|
||||
|
||||
string finalReply = null;
|
||||
var successfulQueryTools = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var successfulActionTools = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var failedActionTools = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
float startTime = Time.realtimeSinceStartup;
|
||||
int maxSteps = Math.Max(1, settings.reactMaxSteps);
|
||||
float maxSeconds = Math.Max(2f, settings.reactMaxSeconds);
|
||||
_thinkingPhaseTotal = maxSteps;
|
||||
|
||||
for (int step = 1; step <= maxSteps; step++)
|
||||
{
|
||||
if (Time.realtimeSinceStartup - startTime > maxSeconds)
|
||||
{
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
WulaLog.Debug("[WulaAI] Native tool loop timed out.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
SetThinkingPhase(step, false);
|
||||
|
||||
ChatCompletionResult result = await client.GetChatCompletionWithToolsAsync(
|
||||
systemInstruction,
|
||||
messages,
|
||||
tools,
|
||||
maxTokens: 2048,
|
||||
temperature: 0.2f);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
AddAssistantMessage("Wula_AI_Error_ConnectionLost".Translate());
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.ToolCalls != null && result.ToolCalls.Count > 0)
|
||||
{
|
||||
int maxTools = ReactMaxToolsPerStep;
|
||||
var callsToExecute = result.ToolCalls.Count > maxTools
|
||||
? result.ToolCalls.Take(maxTools).ToList()
|
||||
: result.ToolCalls;
|
||||
|
||||
messages.Add(ChatMessage.AssistantWithToolCalls(callsToExecute, result.Content));
|
||||
|
||||
int executed = 0;
|
||||
var historyCalls = new List<object>();
|
||||
StringBuilder combinedResults = new StringBuilder();
|
||||
|
||||
if (result.ToolCalls.Count > maxTools)
|
||||
{
|
||||
combinedResults.AppendLine($"ToolRunner Note: Skipped {result.ToolCalls.Count - maxTools} tool call(s) because this step allows at most {maxTools} tool call(s).");
|
||||
}
|
||||
|
||||
foreach (var call in callsToExecute)
|
||||
{
|
||||
if (call == null || string.IsNullOrWhiteSpace(call.Name))
|
||||
{
|
||||
executed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(call.Id))
|
||||
{
|
||||
call.Id = $"call_{step}_{executed + 1}";
|
||||
}
|
||||
|
||||
var historyCall = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "function",
|
||||
["function"] = new Dictionary<string, object>
|
||||
{
|
||||
["name"] = call.Name,
|
||||
["arguments"] = JsonToolCallParser.TryParseObject(call.ArgumentsJson ?? "{}", out var parsedArgs)
|
||||
? (object)parsedArgs
|
||||
: new Dictionary<string, object>()
|
||||
}
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(call.Id))
|
||||
{
|
||||
historyCall["id"] = call.Id;
|
||||
}
|
||||
historyCalls.Add(historyCall);
|
||||
|
||||
var tool = _tools.FirstOrDefault(t => string.Equals(t.Name, call.Name, StringComparison.OrdinalIgnoreCase));
|
||||
if (tool == null)
|
||||
{
|
||||
string missing = $"Error: Tool '{call.Name}' not found.";
|
||||
combinedResults.AppendLine(missing);
|
||||
messages.Add(ChatMessage.ToolResult(call.Id ?? "", missing));
|
||||
executed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
string argsJson = string.IsNullOrWhiteSpace(call.ArgumentsJson) ? "{}" : call.ArgumentsJson;
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
WulaLog.Debug($"[WulaAI] Executing tool (native): {call.Name} with args: {argsJson}");
|
||||
}
|
||||
|
||||
string toolResult = (await tool.ExecuteAsync(argsJson)).Trim();
|
||||
bool isError = !string.IsNullOrEmpty(toolResult) && toolResult.StartsWith("Error:", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (call.Name == "modify_goodwill")
|
||||
{
|
||||
combinedResults.AppendLine($"Tool '{call.Name}' Result (Invisible): {toolResult}");
|
||||
}
|
||||
else
|
||||
{
|
||||
combinedResults.AppendLine($"Tool '{call.Name}' Result: {toolResult}");
|
||||
}
|
||||
|
||||
messages.Add(ChatMessage.ToolResult(call.Id ?? "", toolResult));
|
||||
|
||||
if (!isError)
|
||||
{
|
||||
if (IsActionToolName(call.Name))
|
||||
{
|
||||
successfulActionTools.Add(call.Name);
|
||||
AddActionSuccess(call.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
successfulQueryTools.Add(call.Name);
|
||||
}
|
||||
}
|
||||
else if (IsActionToolName(call.Name))
|
||||
{
|
||||
failedActionTools.Add(call.Name);
|
||||
AddActionFailure(call.Name);
|
||||
}
|
||||
|
||||
executed++;
|
||||
}
|
||||
|
||||
string toolCallsJson = historyCalls.Count == 0
|
||||
? "{\"tool_calls\": []}"
|
||||
: JsonToolCallParser.SerializeToJson(new Dictionary<string, object> { ["tool_calls"] = historyCalls });
|
||||
_history.Add(("toolcall", toolCallsJson));
|
||||
_history.Add(("tool", combinedResults.ToString().Trim()));
|
||||
PersistHistory();
|
||||
UpdateActionLedgerNote();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(result.Content))
|
||||
{
|
||||
finalReply = result.Content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(finalReply) && IsToolCallJson(finalReply))
|
||||
{
|
||||
finalReply = null;
|
||||
}
|
||||
|
||||
_querySuccessfulToolCall = successfulQueryTools.Count > 0;
|
||||
_actionSuccessfulToolCall = successfulActionTools.Count > 0;
|
||||
_queryToolLedgerNote = _querySuccessfulToolCall
|
||||
? $"Tool Ledger (Query): {string.Join(", ", successfulQueryTools)}"
|
||||
: "Tool Ledger (Query): None (no successful tool calls).";
|
||||
_actionToolLedgerNote = _actionSuccessfulToolCall
|
||||
? $"Tool Ledger (Action): {string.Join(", ", successfulActionTools)}"
|
||||
: "Tool Ledger (Action): None (no successful tool calls).";
|
||||
_lastActionHadError = failedActionTools.Count > 0;
|
||||
_lastSuccessfulToolCall = _querySuccessfulToolCall || _actionSuccessfulToolCall;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(finalReply))
|
||||
{
|
||||
string replyInstruction = GetSystemInstruction(false, "");
|
||||
if (!string.IsNullOrWhiteSpace(_queryToolLedgerNote))
|
||||
{
|
||||
replyInstruction += "\n" + _queryToolLedgerNote;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(_actionToolLedgerNote))
|
||||
{
|
||||
replyInstruction += "\n" + _actionToolLedgerNote;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(_lastActionLedgerNote))
|
||||
{
|
||||
replyInstruction += "\n" + _lastActionLedgerNote +
|
||||
"\nIMPORTANT: Do NOT claim any in-game actions beyond the Action Ledger. If the ledger is None, you MUST NOT claim any deliveries, reinforcements, or bombardments.";
|
||||
}
|
||||
if (_lastActionExecuted)
|
||||
{
|
||||
replyInstruction += "\nIMPORTANT: Actions in the Action Ledger were executed in-game. You MUST acknowledge them as completed in your reply. You MUST NOT deny, retract, or contradict them.";
|
||||
}
|
||||
if (!_lastSuccessfulToolCall)
|
||||
{
|
||||
replyInstruction += "\nIMPORTANT: No successful tool calls occurred in the tool phase. You MUST NOT claim any tools or actions succeeded.";
|
||||
}
|
||||
if (_lastActionHadError)
|
||||
{
|
||||
replyInstruction += "\nIMPORTANT: An action tool failed. You MUST acknowledge the failure and MUST NOT claim success.";
|
||||
if (_lastActionExecuted)
|
||||
{
|
||||
replyInstruction += " You MUST still confirm any successful actions separately.";
|
||||
}
|
||||
}
|
||||
|
||||
finalReply = await client.GetChatCompletionAsync(replyInstruction, BuildReplyHistory(), base64Image: null);
|
||||
if (string.IsNullOrEmpty(finalReply))
|
||||
{
|
||||
AddAssistantMessage("Wula_AI_Error_ConnectionLost".Translate());
|
||||
return;
|
||||
}
|
||||
|
||||
bool replyHadToolCalls = IsToolCallJson(finalReply);
|
||||
string strippedReply = StripToolCallJson(finalReply)?.Trim() ?? "";
|
||||
if (replyHadToolCalls || string.IsNullOrWhiteSpace(strippedReply))
|
||||
{
|
||||
string retryReplyInstruction = replyInstruction +
|
||||
"\n\n# RETRY (REPLY OUTPUT)\n" +
|
||||
"Your last reply included tool call JSON or was empty. Tool calls are DISABLED.\n" +
|
||||
"You MUST reply in natural language only. Do NOT output any tool call JSON.\n";
|
||||
string retryReply = await client.GetChatCompletionAsync(retryReplyInstruction, BuildReplyHistory(), maxTokens: 256, temperature: 0.3f);
|
||||
if (!string.IsNullOrEmpty(retryReply))
|
||||
{
|
||||
finalReply = retryReply;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddAssistantMessage(finalReply);
|
||||
TriggerMemoryUpdate();
|
||||
}
|
||||
private async Task<PhaseExecutionResult> ExecuteJsonToolsForStep(string json)
|
||||
{
|
||||
string guidance = "ToolRunner Guidance: Continue with JSON only using {\"thought\":\"...\",\"tool_calls\":[...]}. " +
|
||||
|
||||
@@ -7,9 +7,51 @@ using UnityEngine;
|
||||
using Verse;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using WulaFallenEmpire.EventSystem.AI.Utils;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI
|
||||
{
|
||||
public sealed class ToolCallRequest
|
||||
{
|
||||
public string Id;
|
||||
public string Name;
|
||||
public string ArgumentsJson;
|
||||
}
|
||||
|
||||
public sealed class ChatMessage
|
||||
{
|
||||
public string Role;
|
||||
public string Content;
|
||||
public string ToolCallId;
|
||||
public List<ToolCallRequest> ToolCalls;
|
||||
|
||||
public static ChatMessage User(string content)
|
||||
{
|
||||
return new ChatMessage { Role = "user", Content = content };
|
||||
}
|
||||
|
||||
public static ChatMessage Assistant(string content)
|
||||
{
|
||||
return new ChatMessage { Role = "assistant", Content = content };
|
||||
}
|
||||
|
||||
public static ChatMessage AssistantWithToolCalls(List<ToolCallRequest> toolCalls, string content = null)
|
||||
{
|
||||
return new ChatMessage { Role = "assistant", Content = content, ToolCalls = toolCalls };
|
||||
}
|
||||
|
||||
public static ChatMessage ToolResult(string toolCallId, string content)
|
||||
{
|
||||
return new ChatMessage { Role = "tool", ToolCallId = toolCallId, Content = content };
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ChatCompletionResult
|
||||
{
|
||||
public string Content;
|
||||
public List<ToolCallRequest> ToolCalls;
|
||||
}
|
||||
|
||||
public class SimpleAIClient
|
||||
{
|
||||
private readonly string _apiKey;
|
||||
@@ -125,6 +167,31 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<ChatCompletionResult> GetChatCompletionWithToolsAsync(string instruction, List<ChatMessage> messages, List<Dictionary<string, object>> tools, int? maxTokens = null, float? temperature = null)
|
||||
{
|
||||
if (_useGemini)
|
||||
{
|
||||
WulaLog.Debug("[WulaAI] Native tool calling is not supported with Gemini protocol.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_baseUrl))
|
||||
{
|
||||
WulaLog.Debug("[WulaAI] Base URL is missing.");
|
||||
return null;
|
||||
}
|
||||
|
||||
string endpoint = $"{_baseUrl}/chat/completions";
|
||||
if (_baseUrl.EndsWith("/chat/completions")) endpoint = _baseUrl;
|
||||
else if (!_baseUrl.EndsWith("/v1")) endpoint = $"{_baseUrl}/v1/chat/completions";
|
||||
|
||||
string jsonBody = BuildChatRequestBody(instruction, messages, tools, maxTokens, temperature);
|
||||
string response = await SendRequestRawAsync(endpoint, jsonBody, _apiKey);
|
||||
if (response == null) return null;
|
||||
|
||||
return ExtractChatCompletionResult(response);
|
||||
}
|
||||
|
||||
private async Task<string> GetGeminiCompletionAsync(string instruction, List<(string role, string message)> messages, int? maxTokens = null, float? temperature = null, string base64Image = null)
|
||||
{
|
||||
// Ensure messages is not empty to avoid Gemini 400 Error (Invalid Argument)
|
||||
@@ -185,6 +252,13 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
}
|
||||
|
||||
private async Task<string> SendRequestAsync(string endpoint, string jsonBody, string apiKey)
|
||||
{
|
||||
string response = await SendRequestRawAsync(endpoint, jsonBody, apiKey);
|
||||
if (response == null) return null;
|
||||
return ExtractContent(response);
|
||||
}
|
||||
|
||||
private async Task<string> SendRequestRawAsync(string endpoint, string jsonBody, string apiKey)
|
||||
{
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
@@ -228,10 +302,251 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
{
|
||||
WulaLog.Debug($"[WulaAI] Response Body:\n{TruncateForLog(response)}");
|
||||
}
|
||||
return ExtractContent(response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildChatRequestBody(string instruction, List<ChatMessage> messages, List<Dictionary<string, object>> tools, int? maxTokens, float? temperature)
|
||||
{
|
||||
var body = new Dictionary<string, object>
|
||||
{
|
||||
["model"] = _model,
|
||||
["stream"] = false
|
||||
};
|
||||
|
||||
if (maxTokens.HasValue) body["max_tokens"] = Math.Max(1, maxTokens.Value);
|
||||
if (temperature.HasValue) body["temperature"] = temperature.Value;
|
||||
|
||||
var messageList = new List<object>();
|
||||
if (!string.IsNullOrEmpty(instruction))
|
||||
{
|
||||
messageList.Add(new Dictionary<string, object>
|
||||
{
|
||||
["role"] = "system",
|
||||
["content"] = instruction
|
||||
});
|
||||
}
|
||||
|
||||
if (messages != null)
|
||||
{
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
if (msg == null) continue;
|
||||
string role = string.IsNullOrWhiteSpace(msg.Role) ? "user" : msg.Role;
|
||||
var entry = new Dictionary<string, object>
|
||||
{
|
||||
["role"] = role
|
||||
};
|
||||
|
||||
if (string.Equals(role, "tool", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
entry["tool_call_id"] = msg.ToolCallId ?? "";
|
||||
entry["content"] = msg.Content ?? "";
|
||||
messageList.Add(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(role, "assistant", StringComparison.OrdinalIgnoreCase) && msg.ToolCalls != null && msg.ToolCalls.Count > 0)
|
||||
{
|
||||
var toolCalls = new List<object>();
|
||||
foreach (var call in msg.ToolCalls)
|
||||
{
|
||||
if (call == null) continue;
|
||||
var callEntry = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "function",
|
||||
["function"] = new Dictionary<string, object>
|
||||
{
|
||||
["name"] = call.Name ?? "",
|
||||
["arguments"] = call.ArgumentsJson ?? "{}"
|
||||
}
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(call.Id))
|
||||
{
|
||||
callEntry["id"] = call.Id;
|
||||
}
|
||||
toolCalls.Add(callEntry);
|
||||
}
|
||||
entry["content"] = string.IsNullOrWhiteSpace(msg.Content) ? null : msg.Content;
|
||||
entry["tool_calls"] = toolCalls;
|
||||
messageList.Add(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
entry["content"] = msg.Content ?? "";
|
||||
messageList.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
body["messages"] = messageList;
|
||||
|
||||
if (tools != null && tools.Count > 0)
|
||||
{
|
||||
var toolList = new List<object>();
|
||||
foreach (var tool in tools)
|
||||
{
|
||||
if (tool == null) continue;
|
||||
toolList.Add(tool);
|
||||
}
|
||||
if (toolList.Count > 0)
|
||||
{
|
||||
body["tools"] = toolList;
|
||||
}
|
||||
}
|
||||
|
||||
return JsonToolCallParser.SerializeToJson(body);
|
||||
}
|
||||
|
||||
private ChatCompletionResult ExtractChatCompletionResult(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return null;
|
||||
|
||||
if (!JsonToolCallParser.TryParseObject(json, out var root))
|
||||
{
|
||||
return new ChatCompletionResult { Content = ExtractContent(json) };
|
||||
}
|
||||
|
||||
if (!TryGetList(root, "choices", out var choices) || choices.Count == 0)
|
||||
{
|
||||
return new ChatCompletionResult { Content = ExtractContent(json) };
|
||||
}
|
||||
|
||||
var firstChoice = choices[0] as Dictionary<string, object>;
|
||||
if (firstChoice == null)
|
||||
{
|
||||
return new ChatCompletionResult { Content = ExtractContent(json) };
|
||||
}
|
||||
|
||||
Dictionary<string, object> message = null;
|
||||
if (TryGetObject(firstChoice, "message", out var msgObj))
|
||||
{
|
||||
message = msgObj;
|
||||
}
|
||||
else if (TryGetObject(firstChoice, "delta", out var deltaObj))
|
||||
{
|
||||
message = deltaObj;
|
||||
}
|
||||
|
||||
if (message == null)
|
||||
{
|
||||
return new ChatCompletionResult { Content = ExtractContent(json) };
|
||||
}
|
||||
|
||||
string content = TryGetString(message, "content");
|
||||
var result = new ChatCompletionResult
|
||||
{
|
||||
Content = content,
|
||||
ToolCalls = ParseToolCalls(message)
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<ToolCallRequest> ParseToolCalls(Dictionary<string, object> message)
|
||||
{
|
||||
if (!TryGetList(message, "tool_calls", out var calls) || calls.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var results = new List<ToolCallRequest>();
|
||||
foreach (var callObj in calls)
|
||||
{
|
||||
if (callObj is not Dictionary<string, object> callDict) continue;
|
||||
|
||||
string id = TryGetString(callDict, "id");
|
||||
string name = null;
|
||||
object argsObj = null;
|
||||
|
||||
if (TryGetObject(callDict, "function", out var fnObj))
|
||||
{
|
||||
name = TryGetString(fnObj, "name");
|
||||
TryGetValue(fnObj, "arguments", out argsObj);
|
||||
}
|
||||
else
|
||||
{
|
||||
name = TryGetString(callDict, "name");
|
||||
TryGetValue(callDict, "arguments", out argsObj);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name)) continue;
|
||||
|
||||
string argsJson = "{}";
|
||||
if (argsObj is string argsString)
|
||||
{
|
||||
argsJson = string.IsNullOrWhiteSpace(argsString) ? "{}" : argsString;
|
||||
}
|
||||
else if (argsObj is Dictionary<string, object> argsDict)
|
||||
{
|
||||
argsJson = JsonToolCallParser.SerializeToJson(argsDict);
|
||||
}
|
||||
else if (argsObj != null)
|
||||
{
|
||||
argsJson = JsonToolCallParser.SerializeToJson(argsObj);
|
||||
}
|
||||
|
||||
results.Add(new ToolCallRequest
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
ArgumentsJson = argsJson
|
||||
});
|
||||
}
|
||||
|
||||
return results.Count > 0 ? results : null;
|
||||
}
|
||||
|
||||
private static bool TryGetList(Dictionary<string, object> obj, string key, out List<object> list)
|
||||
{
|
||||
list = null;
|
||||
if (!TryGetValue(obj, key, out object raw)) return false;
|
||||
if (raw is List<object> rawList)
|
||||
{
|
||||
list = rawList;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetObject(Dictionary<string, object> obj, string key, out Dictionary<string, object> value)
|
||||
{
|
||||
value = null;
|
||||
if (!TryGetValue(obj, key, out object raw)) return false;
|
||||
if (raw is Dictionary<string, object> dict)
|
||||
{
|
||||
value = dict;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string TryGetString(Dictionary<string, object> obj, string key)
|
||||
{
|
||||
if (TryGetValue(obj, key, out object value) && value != null)
|
||||
{
|
||||
return Convert.ToString(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryGetValue(Dictionary<string, object> obj, string key, out object value)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
foreach (var kvp in obj)
|
||||
{
|
||||
if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = kvp.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private string ExtractContent(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Verse;
|
||||
@@ -13,10 +14,27 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
public abstract string Name { get; }
|
||||
public abstract string Description { get; }
|
||||
public abstract string UsageSchema { get; } // JSON schema description
|
||||
public abstract Dictionary<string, object> GetParametersSchema();
|
||||
|
||||
public virtual string Execute(string args) => "Error: Synchronous execution not supported for this tool.";
|
||||
public virtual Task<string> ExecuteAsync(string args) => Task.FromResult(Execute(args));
|
||||
|
||||
public virtual Dictionary<string, object> GetFunctionDefinition()
|
||||
{
|
||||
var parameters = GetParametersSchema() ?? SchemaObject(new Dictionary<string, object>(), new string[] { });
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "function",
|
||||
["function"] = new Dictionary<string, object>
|
||||
{
|
||||
["name"] = Name ?? "",
|
||||
["description"] = Description ?? "",
|
||||
["parameters"] = parameters,
|
||||
["strict"] = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to parse JSON arguments into a dictionary.
|
||||
/// </summary>
|
||||
@@ -116,6 +134,64 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
return JsonToolCallParser.LooksLikeJson(input);
|
||||
}
|
||||
|
||||
protected static Dictionary<string, object> SchemaString(string description = null, bool nullable = false)
|
||||
{
|
||||
return SchemaPrimitive("string", description, nullable);
|
||||
}
|
||||
|
||||
protected static Dictionary<string, object> SchemaInteger(string description = null, bool nullable = false)
|
||||
{
|
||||
return SchemaPrimitive("integer", description, nullable);
|
||||
}
|
||||
|
||||
protected static Dictionary<string, object> SchemaNumber(string description = null, bool nullable = false)
|
||||
{
|
||||
return SchemaPrimitive("number", description, nullable);
|
||||
}
|
||||
|
||||
protected static Dictionary<string, object> SchemaBoolean(string description = null, bool nullable = false)
|
||||
{
|
||||
return SchemaPrimitive("boolean", description, nullable);
|
||||
}
|
||||
|
||||
protected static Dictionary<string, object> SchemaArray(object itemSchema, string description = null, bool nullable = false)
|
||||
{
|
||||
var schema = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = nullable ? new List<object> { "array", "null" } : "array",
|
||||
["items"] = itemSchema
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
schema["description"] = description;
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
protected static Dictionary<string, object> SchemaObject(Dictionary<string, object> properties, IEnumerable<string> required, string description = null, bool nullable = false)
|
||||
{
|
||||
var schema = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = nullable ? new List<object> { "object", "null" } : "object",
|
||||
["properties"] = properties ?? new Dictionary<string, object>(),
|
||||
["additionalProperties"] = false
|
||||
};
|
||||
if (required != null)
|
||||
{
|
||||
schema["required"] = required.Select(r => (object)r).ToList();
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
schema["description"] = description;
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
protected static List<string> RequiredList(params string[] fields)
|
||||
{
|
||||
return fields?.Where(f => !string.IsNullOrWhiteSpace(f)).ToList() ?? new List<string>();
|
||||
}
|
||||
|
||||
private static bool TryGetNumber(Dictionary<string, object> args, string key, out double value)
|
||||
{
|
||||
value = 0;
|
||||
@@ -148,5 +224,18 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> SchemaPrimitive(string type, string description, bool nullable)
|
||||
{
|
||||
var schema = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = nullable ? new List<object> { type, "null" } : type
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
schema["description"] = description;
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,15 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
"Analyze the current game screen screenshot. Provide an instruction to guide the analysis.";
|
||||
|
||||
public override string UsageSchema => "{\"instruction\":\"Describe the current screen\"}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["instruction"] = SchemaString("Instruction for image analysis.", nullable: true),
|
||||
["context"] = SchemaString("Alias for instruction.", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("instruction", "context"));
|
||||
}
|
||||
|
||||
private const string BaseVisionSystemPrompt = "You are a seasoned RimWorld assistant. Analyze the screenshot per instruction. Keep replies concise. Do not output tool call JSON unless explicitly asked.";
|
||||
|
||||
|
||||
@@ -14,6 +14,31 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
public override string Name => "call_bombardment";
|
||||
public override string Description => "Calls orbital bombardment/support using an AbilityDef configuration (e.g., WULA_Firepower_Cannon_Salvo, WULA_Firepower_EnergyLance_Strafe). Supports Circular Bombardment, Strafe, Energy Lance, and Surveillance.";
|
||||
public override string UsageSchema => "{\"abilityDef\":\"WULA_Firepower_Cannon_Salvo\",\"x\":12,\"z\":34,\"direction\":\"20,30\",\"angle\":90,\"filterFriendlyFire\":true}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["abilityDef"] = SchemaString("AbilityDef defName.", nullable: true),
|
||||
["x"] = SchemaInteger("Target cell X.", nullable: true),
|
||||
["z"] = SchemaInteger("Target cell Z.", nullable: true),
|
||||
["cell"] = SchemaString("Target cell formatted as 'x,z'.", nullable: true),
|
||||
["direction"] = SchemaString("Direction cell 'x,z' for strafes.", nullable: true),
|
||||
["angle"] = SchemaNumber("Angle for strafe/lance direction.", nullable: true),
|
||||
["filterFriendlyFire"] = SchemaBoolean("Avoid friendly fire if possible.", nullable: true),
|
||||
["dirX"] = SchemaInteger("Direction cell X.", nullable: true),
|
||||
["dirZ"] = SchemaInteger("Direction cell Z.", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList(
|
||||
"abilityDef",
|
||||
"x",
|
||||
"z",
|
||||
"cell",
|
||||
"direction",
|
||||
"angle",
|
||||
"filterFriendlyFire",
|
||||
"dirX",
|
||||
"dirZ"));
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
@@ -17,6 +17,17 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
"TIP: Use the 'get_available_prefabs' tool first to see which structures are available. " +
|
||||
"The default skyfaller animation is 'WULA_Prefab_Incoming'.";
|
||||
public override string UsageSchema => "{\"prefabDefName\":\"WULA_NewColonyBase\",\"skyfallerDef\":\"WULA_Prefab_Incoming\",\"x\":10,\"z\":20}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["prefabDefName"] = SchemaString("PrefabDef defName.", nullable: true),
|
||||
["skyfallerDef"] = SchemaString("Skyfaller ThingDef defName.", nullable: true),
|
||||
["x"] = SchemaInteger("Target cell X.", nullable: true),
|
||||
["z"] = SchemaInteger("Target cell Z.", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("prefabDefName", "skyfallerDef", "x", "z"));
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Verse;
|
||||
using WulaFallenEmpire.EventSystem.AI;
|
||||
|
||||
@@ -9,6 +10,14 @@ 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\": 2}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["expression_id"] = SchemaInteger("Expression id (1-6).", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("expression_id"));
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
@@ -13,6 +13,10 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
public override string Description => "Returns a list of available orbital bombardment abilities (AbilityDefs) that can be called. " +
|
||||
"Use this to find the correct 'abilityDef' for the 'call_bombardment' tool.";
|
||||
public override string UsageSchema => "{}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
return SchemaObject(new Dictionary<string, object>(), RequiredList());
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
@@ -13,6 +13,10 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
public override string Description => "Returns a list of available building prefabs (blueprints) that can be summoned. " +
|
||||
"Use this to find the correct 'prefabDefName' for the 'call_prefab_airdrop' tool.";
|
||||
public override string UsageSchema => "{}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
return SchemaObject(new Dictionary<string, object>(), RequiredList());
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
@@ -13,6 +13,16 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
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 =>
|
||||
"{\"filter\":\"friendly,hostile,colonist\",\"includeDead\":true,\"maxResults\":50}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["filter"] = SchemaString("Comma-separated filters (friendly, hostile, colonist, etc.).", nullable: true),
|
||||
["includeDead"] = SchemaBoolean("Include corpses.", nullable: true),
|
||||
["maxResults"] = SchemaInteger("Max results to show.", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("filter", "includeDead", "maxResults"));
|
||||
}
|
||||
|
||||
private struct MapPawnEntry
|
||||
{
|
||||
|
||||
@@ -13,6 +13,14 @@ 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\":\"Steel\"}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["resourceName"] = SchemaString("Thing label or defName to check.", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("resourceName"));
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,16 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
public override string Name => "get_pawn_status";
|
||||
public override string Description => "Returns detailed status (health, needs, gear) of specified pawns. Use this to check for sickness, injuries, mood, or equipment. Can filter by name, category (colonist/animal/prisoner/guest), or status (sick/injured).";
|
||||
public override string UsageSchema => "{\"name\":\"optional\",\"category\":\"colonist\",\"filter\":\"sick\"}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["name"] = SchemaString("Name filter (substring).", nullable: true),
|
||||
["category"] = SchemaString("colonist/animal/prisoner/guest/all.", nullable: true),
|
||||
["filter"] = SchemaString("sick/injured/downed/dead.", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("name", "category", "filter"));
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
@@ -17,6 +17,16 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
public override string Description => "Returns the most recent letters and messages, sorted by in-game time from newest to oldest.";
|
||||
public override string UsageSchema =>
|
||||
"{\"count\":10,\"includeLetters\":true,\"includeMessages\":true}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["count"] = SchemaInteger("Max notifications to return.", nullable: true),
|
||||
["includeLetters"] = SchemaBoolean("Include letters.", nullable: true),
|
||||
["includeMessages"] = SchemaBoolean("Include messages.", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("count", "includeLetters", "includeMessages"));
|
||||
}
|
||||
|
||||
private struct NotificationEntry
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
@@ -9,6 +10,14 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
public override string Name => "modify_goodwill";
|
||||
public override string Description => "Adjusts YOUR internal opinion of the player (AI Goodwill). WARNING: This DOES NOT affect Faction Relations or stop raids. It is purely personal. Do NOT use this to try to stop enemies.";
|
||||
public override string UsageSchema => "{\"amount\": 1}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["amount"] = SchemaInteger("Change in goodwill (-5 to 5).", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("amount"));
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,15 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
public override string Name => "recall_memories";
|
||||
public override string Description => "Searches the AI's long-term memory for facts matching a specific query or keyword.";
|
||||
public override string UsageSchema => "{\"query\":\"keywords\",\"limit\":5}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["query"] = SchemaString("Search query.", nullable: true),
|
||||
["limit"] = SchemaInteger("Max memories to return.", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("query", "limit"));
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,15 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
public override string Name => "remember_fact";
|
||||
public override string Description => "Stores a specific fact or piece of information into the AI's long-term memory for future retrieval.";
|
||||
public override string UsageSchema => "{\"fact\":\"...\",\"category\":\"misc\"}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["fact"] = SchemaString("Fact to store.", nullable: true),
|
||||
["category"] = SchemaString("Memory category.", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("fact", "category"));
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
@@ -11,6 +12,16 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
public override string Name => "search_pawn_kind";
|
||||
public override string Description => "Rough-searches PawnKindDefs by natural language (label/defName). Returns candidate defNames for send_reinforcement.";
|
||||
public override string UsageSchema => "{\"query\":\"escort\",\"maxResults\":10,\"minScore\":0.15}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["query"] = SchemaString("Search query.", nullable: true),
|
||||
["maxResults"] = SchemaInteger("Max candidates to return.", nullable: true),
|
||||
["minScore"] = SchemaNumber("Minimum similarity score.", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("query", "maxResults", "minScore"));
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using RimWorld;
|
||||
@@ -12,6 +13,16 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
public override string Name => "search_thing_def";
|
||||
public override string Description => "Rough-searches RimWorld ThingDefs by natural language (label/defName). Returns candidate defNames so you can use them in other tools like spawn_resources.";
|
||||
public override string UsageSchema => "{\"query\":\"Steel\",\"maxResults\":10,\"itemsOnly\":true}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["query"] = SchemaString("Search query.", nullable: true),
|
||||
["maxResults"] = SchemaInteger("Max candidates to return.", nullable: true),
|
||||
["itemsOnly"] = SchemaBoolean("Restrict to item defs.", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("query", "maxResults", "itemsOnly"));
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
@@ -104,6 +104,14 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
}
|
||||
|
||||
public override string UsageSchema => "{\"units\": \"Wula_PIA_Heavy_Unit_Melee: 2, Wula_PIA_Legion_Escort_Unit: 5\"}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["units"] = SchemaString("Comma-separated list of PawnKindDef: count.", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("units"));
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,15 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
public override string Name => "set_overwatch_mode";
|
||||
public override string Description => "Enables or disables the AI Overwatch Combat Protocol. When enabled (enabled=true), the AI will autonomously scan for hostile targets every few seconds and launch appropriate orbital bombardments for a set duration. When disabled (enabled=false), it immediately stops any active overwatch and clears the flight path. Use enabled=false to stop overwatch early if the player requests it.";
|
||||
public override string UsageSchema => "{\"enabled\":true,\"durationSeconds\":60}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["enabled"] = SchemaBoolean("Enable or disable overwatch.", nullable: true),
|
||||
["durationSeconds"] = SchemaInteger("Duration in seconds when enabling.", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("enabled", "durationSeconds"));
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
@@ -19,6 +19,30 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
"Otherwise, give a moderate amount. " +
|
||||
"TIP: Use the `search_thing_def` tool first and then spawn by DefName to avoid language mismatch.";
|
||||
public override string UsageSchema => "{\"items\":[{\"name\":\"Steel\",\"count\":100,\"stuffDefName\":\"Steel\"}]}";
|
||||
public override Dictionary<string, object> GetParametersSchema()
|
||||
{
|
||||
var itemProperties = new Dictionary<string, object>
|
||||
{
|
||||
["name"] = SchemaString("Thing defName or label.", nullable: true),
|
||||
["defName"] = SchemaString("ThingDef defName alias for name.", nullable: true),
|
||||
["count"] = SchemaInteger("Stack count.", nullable: true),
|
||||
["stuffDefName"] = SchemaString("Stuff defName for made-from-stuff items.", nullable: true),
|
||||
["stuff"] = SchemaString("Alias for stuffDefName.", nullable: true),
|
||||
["material"] = SchemaString("Alias for stuffDefName.", nullable: true)
|
||||
};
|
||||
var itemSchema = SchemaObject(itemProperties, RequiredList("name", "defName", "count", "stuffDefName", "stuff", "material"));
|
||||
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["items"] = SchemaArray(itemSchema, "List of items to spawn.", nullable: true),
|
||||
["name"] = SchemaString("Single item defName or label.", nullable: true),
|
||||
["count"] = SchemaInteger("Single item count.", nullable: true),
|
||||
["stuffDefName"] = SchemaString("Stuff defName for single item.", nullable: true),
|
||||
["stuff"] = SchemaString("Alias for stuffDefName.", nullable: true),
|
||||
["material"] = SchemaString("Alias for stuffDefName.", nullable: true)
|
||||
};
|
||||
return SchemaObject(properties, RequiredList("items", "name", "count", "stuffDefName", "stuff", "material"));
|
||||
}
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user