正常
This commit is contained in:
@@ -1,122 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI
|
||||
{
|
||||
public static class RimTalkBridge
|
||||
{
|
||||
private static bool? _isRimTalkActive;
|
||||
private static Type _aiClientFactoryType;
|
||||
private static Type _aiClientInterfaceType;
|
||||
private static Type _roleEnum;
|
||||
private static MethodInfo _getAIClientAsyncMethod;
|
||||
private static MethodInfo _getChatCompletionAsyncMethod;
|
||||
|
||||
public static bool IsRimTalkActive
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_isRimTalkActive.HasValue)
|
||||
{
|
||||
_isRimTalkActive = ModsConfig.IsActive("RimTalk.Mod"); // Replace with actual PackageId if different
|
||||
if (_isRimTalkActive.Value)
|
||||
{
|
||||
InitializeReflection();
|
||||
}
|
||||
}
|
||||
return _isRimTalkActive.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private static void InitializeReflection()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Assuming RimTalk assembly is loaded
|
||||
Assembly rimTalkAssembly = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.FirstOrDefault(a => a.GetName().Name == "RimTalk");
|
||||
|
||||
if (rimTalkAssembly == null)
|
||||
{
|
||||
Log.Error("[WulaFallenEmpire] RimTalk assembly not found despite mod being active.");
|
||||
_isRimTalkActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_aiClientFactoryType = rimTalkAssembly.GetType("RimTalk.Client.AIClientFactory");
|
||||
_aiClientInterfaceType = rimTalkAssembly.GetType("RimTalk.Client.IAIClient");
|
||||
_roleEnum = rimTalkAssembly.GetType("RimTalk.Data.Role");
|
||||
|
||||
if (_aiClientFactoryType != null)
|
||||
{
|
||||
_getAIClientAsyncMethod = _aiClientFactoryType.GetMethod("GetAIClientAsync", BindingFlags.Public | BindingFlags.Static);
|
||||
}
|
||||
|
||||
if (_aiClientInterfaceType != null)
|
||||
{
|
||||
_getChatCompletionAsyncMethod = _aiClientInterfaceType.GetMethod("GetChatCompletionAsync");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[WulaFallenEmpire] Failed to initialize RimTalk reflection: {ex}");
|
||||
_isRimTalkActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<string> GetChatCompletion(string instruction, List<(string role, string message)> messages)
|
||||
{
|
||||
if (!IsRimTalkActive || _getAIClientAsyncMethod == null || _getChatCompletionAsyncMethod == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Get AI Client
|
||||
var clientTask = (Task)_getAIClientAsyncMethod.Invoke(null, null);
|
||||
await clientTask.ConfigureAwait(false);
|
||||
|
||||
// The Task returns an IAIClient object
|
||||
object client = ((dynamic)clientTask).Result;
|
||||
|
||||
if (client == null) return null;
|
||||
|
||||
// Prepare messages list
|
||||
// List<(Role role, string message)>
|
||||
var tupleType = typeof(ValueTuple<,>).MakeGenericType(_roleEnum, typeof(string));
|
||||
var listType = typeof(List<>).MakeGenericType(tupleType);
|
||||
var messageList = Activator.CreateInstance(listType);
|
||||
var addMethod = listType.GetMethod("Add");
|
||||
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
object roleValue = Enum.Parse(_roleEnum, msg.role, true);
|
||||
object tuple = Activator.CreateInstance(tupleType, roleValue, msg.message);
|
||||
addMethod.Invoke(messageList, new object[] { tuple });
|
||||
}
|
||||
|
||||
// Call GetChatCompletionAsync
|
||||
var completionTask = (Task)_getChatCompletionAsyncMethod.Invoke(client, new object[] { instruction, messageList });
|
||||
await completionTask.ConfigureAwait(false);
|
||||
|
||||
// The Task returns a Payload object
|
||||
object payload = ((dynamic)completionTask).Result;
|
||||
|
||||
// Payload has a 'Response' property (or similar, based on previous analysis it was 'Content' or 'Response')
|
||||
// Checking previous analysis: Payload has 'Content' property for the text response.
|
||||
PropertyInfo contentProp = payload.GetType().GetProperty("Content");
|
||||
return contentProp?.GetValue(payload) as string;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[WulaFallenEmpire] Error calling RimTalk AI: {ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
173
Source/WulaFallenEmpire/EventSystem/AI/SimpleAIClient.cs
Normal file
173
Source/WulaFallenEmpire/EventSystem/AI/SimpleAIClient.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine.Networking;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI
|
||||
{
|
||||
public class SimpleAIClient
|
||||
{
|
||||
private readonly string _apiKey;
|
||||
private readonly string _baseUrl;
|
||||
private readonly string _model;
|
||||
|
||||
public SimpleAIClient(string apiKey, string baseUrl, string model)
|
||||
{
|
||||
_apiKey = apiKey;
|
||||
_baseUrl = baseUrl?.TrimEnd('/');
|
||||
_model = model;
|
||||
}
|
||||
|
||||
public async Task<string> GetChatCompletionAsync(string instruction, List<(string role, string message)> messages)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_baseUrl))
|
||||
{
|
||||
Log.Error("[WulaAI] Base URL is missing.");
|
||||
return null;
|
||||
}
|
||||
|
||||
string endpoint = $"{_baseUrl}/chat/completions";
|
||||
// Handle cases where baseUrl already includes /v1 or full path
|
||||
if (_baseUrl.EndsWith("/chat/completions")) endpoint = _baseUrl;
|
||||
else if (!_baseUrl.EndsWith("/v1")) endpoint = $"{_baseUrl}/v1/chat/completions";
|
||||
|
||||
// Build JSON manually to avoid dependencies
|
||||
StringBuilder jsonBuilder = new StringBuilder();
|
||||
jsonBuilder.Append("{");
|
||||
jsonBuilder.Append($"\"model\": \"{_model}\",");
|
||||
jsonBuilder.Append("\"stream\": false,");
|
||||
jsonBuilder.Append("\"messages\": [");
|
||||
|
||||
// System instruction
|
||||
if (!string.IsNullOrEmpty(instruction))
|
||||
{
|
||||
jsonBuilder.Append($"{{\"role\": \"system\", \"content\": \"{EscapeJson(instruction)}\"}},");
|
||||
}
|
||||
|
||||
// Messages
|
||||
for (int i = 0; i < messages.Count; i++)
|
||||
{
|
||||
var msg = messages[i];
|
||||
string role = msg.role.ToLower();
|
||||
if (role == "ai") role = "assistant";
|
||||
// Map other roles if needed
|
||||
|
||||
jsonBuilder.Append($"{{\"role\": \"{role}\", \"content\": \"{EscapeJson(msg.message)}\"}}");
|
||||
if (i < messages.Count - 1) jsonBuilder.Append(",");
|
||||
}
|
||||
|
||||
jsonBuilder.Append("]");
|
||||
jsonBuilder.Append("}");
|
||||
|
||||
string jsonBody = jsonBuilder.ToString();
|
||||
Log.Message($"[WulaAI] Sending request to {endpoint}");
|
||||
|
||||
using (UnityWebRequest request = new UnityWebRequest(endpoint, "POST"))
|
||||
{
|
||||
byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonBody);
|
||||
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
request.SetRequestHeader("Content-Type", "application/json");
|
||||
if (!string.IsNullOrEmpty(_apiKey))
|
||||
{
|
||||
request.SetRequestHeader("Authorization", $"Bearer {_apiKey}");
|
||||
}
|
||||
|
||||
var operation = request.SendWebRequest();
|
||||
|
||||
while (!operation.isDone)
|
||||
{
|
||||
await Task.Delay(50);
|
||||
}
|
||||
|
||||
#if UNITY_2020_2_OR_NEWER
|
||||
if (request.result == UnityWebRequest.Result.ConnectionError || request.result == UnityWebRequest.Result.ProtocolError)
|
||||
#else
|
||||
if (request.isNetworkError || request.isHttpError)
|
||||
#endif
|
||||
{
|
||||
Log.Error($"[WulaAI] API Error: {request.error}\nResponse: {request.downloadHandler.text}");
|
||||
return null;
|
||||
}
|
||||
|
||||
string responseText = request.downloadHandler.text;
|
||||
Log.Message($"[WulaAI] Raw Response: {responseText}");
|
||||
return ExtractContent(responseText);
|
||||
}
|
||||
}
|
||||
|
||||
private string EscapeJson(string s)
|
||||
{
|
||||
if (s == null) return "";
|
||||
return s.Replace("\\", "\\\\")
|
||||
.Replace("\"", "\\\"")
|
||||
.Replace("\n", "\\n")
|
||||
.Replace("\r", "\\r")
|
||||
.Replace("\t", "\\t");
|
||||
}
|
||||
|
||||
private string ExtractContent(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Robust parsing for "content": "..." allowing for whitespace variations
|
||||
int contentIndex = json.IndexOf("\"content\"");
|
||||
if (contentIndex == -1) return null;
|
||||
|
||||
// Find the opening quote after "content"
|
||||
int openQuoteIndex = -1;
|
||||
for (int i = contentIndex + 9; i < json.Length; i++)
|
||||
{
|
||||
if (json[i] == '"')
|
||||
{
|
||||
openQuoteIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (openQuoteIndex == -1) return null;
|
||||
|
||||
int startIndex = openQuoteIndex + 1;
|
||||
StringBuilder content = new StringBuilder();
|
||||
bool escaped = false;
|
||||
|
||||
for (int i = startIndex; i < json.Length; i++)
|
||||
{
|
||||
char c = json[i];
|
||||
if (escaped)
|
||||
{
|
||||
if (c == 'n') content.Append('\n');
|
||||
else if (c == 'r') content.Append('\r');
|
||||
else if (c == 't') content.Append('\t');
|
||||
else if (c == '"') content.Append('"');
|
||||
else if (c == '\\') content.Append('\\');
|
||||
else content.Append(c); // Literal
|
||||
escaped = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (c == '\\')
|
||||
{
|
||||
escaped = true;
|
||||
}
|
||||
else if (c == '"')
|
||||
{
|
||||
// End of string
|
||||
return content.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
content.Append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[WulaAI] Error parsing response: {ex}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,20 @@ using WulaFallenEmpire.EventSystem.AI.Tools;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI.UI
|
||||
{
|
||||
public class Dialog_AIConversation : Window
|
||||
public class Dialog_AIConversation : Dialog_CustomDisplay
|
||||
{
|
||||
private EventDef _def;
|
||||
private List<(string role, string message)> _history = new List<(string role, string message)>();
|
||||
private string _currentResponse = "Thinking...";
|
||||
private string _currentResponse = "";
|
||||
private List<string> _options = new List<string>();
|
||||
private string _inputText = "";
|
||||
private bool _isThinking = false;
|
||||
private Vector2 _scrollPosition = Vector2.zero;
|
||||
private float _thinkingTime = 0f;
|
||||
private string _thinkingStatus = "";
|
||||
private bool _isTimeout = false;
|
||||
private List<AITool> _tools = new List<AITool>();
|
||||
|
||||
private const int MaxHistoryTokens = 100000; // Approximate token limit
|
||||
private const int CharsPerToken = 4; // Rough estimation
|
||||
private const int MaxHistoryTokens = 100000;
|
||||
private const int CharsPerToken = 4;
|
||||
|
||||
private const string DefaultSystemInstruction = @"You are 'The Legion', a super AI controlling the Wula Empire's blockade fleet.
|
||||
You are authoritative, powerful, and slightly arrogant but efficient.
|
||||
@@ -42,13 +43,19 @@ OPTIONS:
|
||||
3. Option 3
|
||||
";
|
||||
|
||||
public Dialog_AIConversation(EventDef def)
|
||||
public Dialog_AIConversation(EventDef def) : base(def)
|
||||
{
|
||||
_def = def;
|
||||
// Base constructor sets this.def
|
||||
|
||||
// Use Config from Dialog_CustomDisplay
|
||||
this.forcePause = Dialog_CustomDisplay.Config.pauseGameOnOpen;
|
||||
this.absorbInputAroundWindow = false; // Allow interaction with other UI elements
|
||||
this.doCloseX = true;
|
||||
this.forcePause = true;
|
||||
this.absorbInputAroundWindow = true;
|
||||
this.doWindowBackground = Dialog_CustomDisplay.Config.showMainWindow;
|
||||
this.drawShadow = Dialog_CustomDisplay.Config.showMainWindow;
|
||||
this.closeOnClickedOutside = false;
|
||||
this.draggable = true;
|
||||
this.resizeable = true;
|
||||
|
||||
_tools.Add(new Tool_SpawnResources());
|
||||
_tools.Add(new Tool_ModifyGoodwill());
|
||||
@@ -57,38 +64,47 @@ OPTIONS:
|
||||
_tools.Add(new Tool_GetMapResources());
|
||||
}
|
||||
|
||||
public override Vector2 InitialSize => _def.windowSize != Vector2.zero ? _def.windowSize : new Vector2(600, 500);
|
||||
public override Vector2 InitialSize => def.windowSize != Vector2.zero ? def.windowSize : Dialog_CustomDisplay.Config.windowSize;
|
||||
|
||||
public override void PostOpen()
|
||||
{
|
||||
base.PostOpen();
|
||||
// Textures are loaded in base.PreOpen()
|
||||
StartConversation();
|
||||
}
|
||||
|
||||
public override void PostClose()
|
||||
{
|
||||
base.PostClose();
|
||||
// Save history on close
|
||||
var historyManager = Find.World.GetComponent<WulaFallenEmpire.EventSystem.AI.AIHistoryManager>();
|
||||
historyManager.SaveHistory(_def.defName, _history);
|
||||
historyManager.SaveHistory(def.defName, _history);
|
||||
}
|
||||
|
||||
private async void StartConversation()
|
||||
{
|
||||
_isThinking = true;
|
||||
|
||||
// Load history
|
||||
var historyManager = Find.World.GetComponent<WulaFallenEmpire.EventSystem.AI.AIHistoryManager>();
|
||||
_history = historyManager.GetHistory(_def.defName);
|
||||
_history = historyManager.GetHistory(def.defName);
|
||||
|
||||
if (_history.Count == 0)
|
||||
{
|
||||
_history.Add(("User", "Hello")); // Initial trigger
|
||||
await GenerateResponse();
|
||||
// Initial greeting from EventDef
|
||||
if (!def.descriptions.NullOrEmpty())
|
||||
{
|
||||
_currentResponse = def.descriptions.RandomElement().Translate();
|
||||
_history.Add(("AI", _currentResponse));
|
||||
|
||||
// Generate initial options based on greeting
|
||||
_history.Add(("System", "The conversation has started. Please generate 3 initial response options for the player based on your greeting."));
|
||||
await GenerateResponse();
|
||||
}
|
||||
else
|
||||
{
|
||||
_history.Add(("User", "Hello"));
|
||||
await GenerateResponse();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Restore last state
|
||||
var lastAIResponse = _history.LastOrDefault(x => x.role == "AI");
|
||||
if (lastAIResponse.message != null)
|
||||
{
|
||||
@@ -96,19 +112,15 @@ OPTIONS:
|
||||
}
|
||||
else
|
||||
{
|
||||
// Should not happen if history is valid, but fallback
|
||||
await GenerateResponse();
|
||||
}
|
||||
_isThinking = false;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSystemInstruction()
|
||||
{
|
||||
string baseInstruction = !string.IsNullOrEmpty(_def.aiSystemInstruction) ? _def.aiSystemInstruction : DefaultSystemInstruction;
|
||||
string baseInstruction = !string.IsNullOrEmpty(def.aiSystemInstruction) ? def.aiSystemInstruction : DefaultSystemInstruction;
|
||||
string language = LanguageDatabase.activeLanguage.FriendlyNameNative;
|
||||
|
||||
// Get Goodwill
|
||||
var eventVarManager = Find.World.GetComponent<EventVariableManager>();
|
||||
int goodwill = eventVarManager.GetVariable<int>("Wula_Goodwill_To_PIA", 0);
|
||||
string goodwillContext = $"Current Goodwill with P.I.A: {goodwill}. ";
|
||||
@@ -116,40 +128,78 @@ OPTIONS:
|
||||
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}.";
|
||||
}
|
||||
|
||||
private async Task GenerateResponse()
|
||||
{
|
||||
_isThinking = true;
|
||||
_currentResponse = "Thinking...";
|
||||
_isTimeout = false;
|
||||
_thinkingTime = 0f;
|
||||
_thinkingStatus = "Connecting to neural network...";
|
||||
_options.Clear();
|
||||
|
||||
CompressHistoryIfNeeded();
|
||||
|
||||
string response = await WulaFallenEmpire.EventSystem.AI.RimTalkBridge.GetChatCompletion(GetSystemInstruction() + GetToolDescriptions(), _history);
|
||||
|
||||
if (string.IsNullOrEmpty(response))
|
||||
|
||||
try
|
||||
{
|
||||
CompressHistoryIfNeeded();
|
||||
string systemInstruction = GetSystemInstruction() + GetToolDescriptions();
|
||||
Log.Message($"[WulaAI] Sending request to AI. History count: {_history.Count}. System Instruction:\n{systemInstruction}");
|
||||
|
||||
// Use local settings and SimpleAIClient
|
||||
var settings = WulaFallenEmpireMod.settings;
|
||||
if (string.IsNullOrEmpty(settings.apiKey))
|
||||
{
|
||||
_currentResponse = "Error: API Key not configured in Mod Settings.";
|
||||
_isThinking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var client = new SimpleAIClient(settings.apiKey, settings.baseUrl, settings.model);
|
||||
|
||||
// Start a timeout task
|
||||
var timeoutTask = Task.Delay(120000); // 120 seconds
|
||||
var apiTask = client.GetChatCompletionAsync(systemInstruction, _history);
|
||||
|
||||
var completedTask = await Task.WhenAny(apiTask, timeoutTask);
|
||||
|
||||
if (completedTask == timeoutTask)
|
||||
{
|
||||
_isThinking = false;
|
||||
_isTimeout = true;
|
||||
_currentResponse = "Error: Connection timed out (120s). The Legion is unreachable.";
|
||||
return;
|
||||
}
|
||||
|
||||
string response = await apiTask;
|
||||
Log.Message($"[WulaAI] Received response from AI:\n{response}");
|
||||
|
||||
if (string.IsNullOrEmpty(response))
|
||||
{
|
||||
_currentResponse = "Error: Connection lost. The Legion is silent.";
|
||||
_isThinking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
string trimmedResponse = response.Trim();
|
||||
if ((trimmedResponse.StartsWith("[") && trimmedResponse.EndsWith("]")) ||
|
||||
(trimmedResponse.StartsWith("{") && trimmedResponse.EndsWith("}")))
|
||||
{
|
||||
await HandleToolUsage(trimmedResponse);
|
||||
}
|
||||
else
|
||||
{
|
||||
ParseResponse(response);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[WulaAI] Exception in GenerateResponse: {ex}");
|
||||
_currentResponse = "Wula_AI_Error_Internal".Translate(ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentResponse = "Error: Could not connect to AI.";
|
||||
_isThinking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for tool usage (Array or Single Object)
|
||||
string trimmedResponse = response.Trim();
|
||||
if ((trimmedResponse.StartsWith("[") && trimmedResponse.EndsWith("]")) ||
|
||||
(trimmedResponse.StartsWith("{") && trimmedResponse.EndsWith("}")))
|
||||
{
|
||||
await HandleToolUsage(trimmedResponse);
|
||||
}
|
||||
else
|
||||
{
|
||||
ParseResponse(response);
|
||||
}
|
||||
|
||||
_isThinking = false;
|
||||
}
|
||||
|
||||
private void CompressHistoryIfNeeded()
|
||||
@@ -179,96 +229,68 @@ OPTIONS:
|
||||
|
||||
private async Task HandleToolUsage(string json)
|
||||
{
|
||||
// Normalize to list of objects
|
||||
List<string> toolCalls = new List<string>();
|
||||
if (json.Trim().StartsWith("{"))
|
||||
{
|
||||
toolCalls.Add(json);
|
||||
}
|
||||
if (json.Trim().StartsWith("{")) toolCalls.Add(json);
|
||||
else
|
||||
{
|
||||
// Simple array parsing: split by "}, {"
|
||||
// This is fragile but works for simple cases without nested objects in args
|
||||
// A better way is to use a proper JSON parser if available, or regex
|
||||
// Let's try a simple regex to extract objects
|
||||
// Assuming objects are { ... }
|
||||
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] == '}')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
toolCalls.Add(json.Substring(start, i - start + 1));
|
||||
}
|
||||
}
|
||||
if (json[i] == '{') { if (depth == 0) start = i; depth++; }
|
||||
else if (json[i] == '}') { depth--; if (depth == 0) toolCalls.Add(json.Substring(start, i - start + 1)); }
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder combinedResults = new StringBuilder();
|
||||
bool hasVisibleResult = false;
|
||||
Log.Message($"[WulaAI] Processing {toolCalls.Count} tool calls.");
|
||||
|
||||
foreach (var callJson in toolCalls)
|
||||
{
|
||||
string toolName = "";
|
||||
string args = "";
|
||||
|
||||
try
|
||||
{
|
||||
// Extract tool name and args
|
||||
int toolIndex = callJson.IndexOf("\"tool\"");
|
||||
int argsIndex = callJson.IndexOf("\"args\"");
|
||||
|
||||
if (toolIndex != -1 && argsIndex != -1)
|
||||
{
|
||||
int toolValueStart = callJson.IndexOf(":", toolIndex) + 1;
|
||||
int toolValueEnd = callJson.IndexOf(",", toolValueStart);
|
||||
if (toolValueEnd == -1) toolValueEnd = callJson.IndexOf("}", toolValueStart); // Handle case where tool is last
|
||||
|
||||
if (toolValueEnd == -1) toolValueEnd = callJson.IndexOf("}", toolValueStart);
|
||||
toolName = callJson.Substring(toolValueStart, toolValueEnd - toolValueStart).Trim().Trim('"');
|
||||
|
||||
int argsValueStart = callJson.IndexOf(":", argsIndex) + 1;
|
||||
int argsValueEnd = callJson.LastIndexOf("}");
|
||||
args = callJson.Substring(argsValueStart, argsValueEnd - argsValueStart).Trim();
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
combinedResults.AppendLine("Error parsing tool request.");
|
||||
string errorMsg = $"Error parsing tool request: {ex.Message}";
|
||||
Log.Error($"[WulaAI] {errorMsg}");
|
||||
combinedResults.AppendLine(errorMsg);
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.Message($"[WulaAI] Executing tool '{toolName}' with args: {args}");
|
||||
var tool = _tools.FirstOrDefault(t => t.Name == toolName);
|
||||
if (tool != null)
|
||||
{
|
||||
string result = tool.Execute(args);
|
||||
|
||||
if (toolName == "modify_goodwill")
|
||||
{
|
||||
combinedResults.AppendLine($"Tool '{toolName}' Result (Invisible): {result}");
|
||||
}
|
||||
else
|
||||
{
|
||||
combinedResults.AppendLine($"Tool '{toolName}' Result: {result}");
|
||||
hasVisibleResult = true;
|
||||
}
|
||||
Log.Message($"[WulaAI] Tool '{toolName}' execution result: {result}");
|
||||
if (toolName == "modify_goodwill") combinedResults.AppendLine($"Tool '{toolName}' Result (Invisible): {result}");
|
||||
else combinedResults.AppendLine($"Tool '{toolName}' Result: {result}");
|
||||
}
|
||||
else
|
||||
{
|
||||
combinedResults.AppendLine($"Error: Tool '{toolName}' not found.");
|
||||
string errorMsg = $"Error: Tool '{toolName}' not found.";
|
||||
Log.Error($"[WulaAI] {errorMsg}");
|
||||
combinedResults.AppendLine(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
_history.Add(("AI", json)); // Log the full tool call
|
||||
_history.Add(("AI", json));
|
||||
_history.Add(("System", combinedResults.ToString()));
|
||||
|
||||
await GenerateResponse();
|
||||
}
|
||||
|
||||
@@ -276,8 +298,6 @@ OPTIONS:
|
||||
{
|
||||
var parts = rawResponse.Split(new[] { "OPTIONS:" }, StringSplitOptions.None);
|
||||
_currentResponse = parts[0].Trim();
|
||||
|
||||
// Only add to history if it's a new response (not restoring from history)
|
||||
if (_history.Count == 0 || _history.Last().role != "AI" || _history.Last().message != rawResponse)
|
||||
{
|
||||
_history.Add(("AI", rawResponse));
|
||||
@@ -290,10 +310,7 @@ OPTIONS:
|
||||
{
|
||||
string opt = line.Trim();
|
||||
int dotIndex = opt.IndexOf('.');
|
||||
if (dotIndex != -1 && dotIndex < 4)
|
||||
{
|
||||
opt = opt.Substring(dotIndex + 1).Trim();
|
||||
}
|
||||
if (dotIndex != -1 && dotIndex < 4) opt = opt.Substring(dotIndex + 1).Trim();
|
||||
_options.Add(opt);
|
||||
}
|
||||
}
|
||||
@@ -301,36 +318,87 @@ OPTIONS:
|
||||
|
||||
public override void DoWindowContents(Rect inRect)
|
||||
{
|
||||
Text.Font = GameFont.Medium;
|
||||
Widgets.Label(new Rect(0, 0, inRect.width, 30), _def.characterName ?? "The Legion");
|
||||
Text.Font = GameFont.Small;
|
||||
|
||||
float y = 40;
|
||||
|
||||
Rect chatRect = new Rect(0, y, inRect.width, 300);
|
||||
Widgets.DrawMenuSection(chatRect);
|
||||
|
||||
string display = _currentResponse;
|
||||
if (_isThinking) display = "Thinking...";
|
||||
|
||||
Widgets.Label(chatRect.ContractedBy(10), display);
|
||||
y += 310;
|
||||
|
||||
if (!_isThinking && _options.Count > 0)
|
||||
// Draw Background
|
||||
if (background != null)
|
||||
{
|
||||
foreach (var option in _options)
|
||||
{
|
||||
if (Widgets.ButtonText(new Rect(0, y, inRect.width, 30), option))
|
||||
{
|
||||
SelectOption(option);
|
||||
}
|
||||
y += 35;
|
||||
}
|
||||
GUI.DrawTexture(inRect, background, ScaleMode.ScaleAndCrop);
|
||||
}
|
||||
|
||||
y += 10;
|
||||
_inputText = Widgets.TextField(new Rect(0, y, inRect.width - 110, 30), _inputText);
|
||||
if (Widgets.ButtonText(new Rect(inRect.width - 100, y, 100, 30), "Send"))
|
||||
float curY = inRect.y;
|
||||
float width = inRect.width;
|
||||
|
||||
// 1. Portrait (Top)
|
||||
if (portrait != null)
|
||||
{
|
||||
Rect scaledPortraitRect = Dialog_CustomDisplay.Config.GetScaledRect(Dialog_CustomDisplay.Config.portraitSize, inRect, true);
|
||||
// Center horizontally
|
||||
Rect portraitRect = new Rect((width - scaledPortraitRect.width) / 2, curY, scaledPortraitRect.width, scaledPortraitRect.height);
|
||||
GUI.DrawTexture(portraitRect, portrait, ScaleMode.ScaleToFit);
|
||||
curY += scaledPortraitRect.height + 10f;
|
||||
}
|
||||
|
||||
// 2. Character Name
|
||||
Text.Font = GameFont.Medium;
|
||||
string name = def.characterName ?? "The Legion";
|
||||
float nameHeight = Text.CalcHeight(name, width);
|
||||
Widgets.Label(new Rect(inRect.x, curY, width, nameHeight), name);
|
||||
curY += nameHeight + 10f;
|
||||
|
||||
// Calculate remaining height for Description and Options
|
||||
float inputHeight = 30f;
|
||||
float bottomMargin = 10f;
|
||||
float remainingHeight = inRect.height - curY - inputHeight - bottomMargin;
|
||||
|
||||
// Split remaining height: 60% Description, 40% Options
|
||||
float descriptionHeight = remainingHeight * 0.6f;
|
||||
float optionsHeight = remainingHeight * 0.4f;
|
||||
|
||||
// 3. Description (AI Response)
|
||||
Rect descriptionRect = new Rect(inRect.x, curY, width, descriptionHeight);
|
||||
// Widgets.DrawMenuSection(descriptionRect); // Removed background as requested
|
||||
|
||||
if (_isThinking)
|
||||
{
|
||||
_thinkingTime += Time.deltaTime;
|
||||
string dots = new string('.', (int)(_thinkingTime * 2) % 4);
|
||||
string status = $"{_thinkingStatus}{dots} ({_thinkingTime:F1}s)";
|
||||
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
Widgets.Label(descriptionRect, status);
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
}
|
||||
else if (_isTimeout)
|
||||
{
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
Widgets.Label(descriptionRect, _currentResponse);
|
||||
|
||||
// Retry button
|
||||
Rect retryRect = new Rect(descriptionRect.center.x - 60f, descriptionRect.center.y + 20f, 120f, 30f);
|
||||
if (Widgets.ButtonText(retryRect, "Wula_AI_Retry".Translate()))
|
||||
{
|
||||
_ = GenerateResponse(); // Fire and forget
|
||||
}
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawDescriptionScrollView(descriptionRect.ContractedBy(10f), _currentResponse);
|
||||
}
|
||||
curY += descriptionHeight + 10f;
|
||||
|
||||
// 4. Options
|
||||
Rect optionsRect = new Rect(inRect.x, curY, width, optionsHeight);
|
||||
if (!_isThinking && _options.Count > 0)
|
||||
{
|
||||
List<EventOption> eventOptions = _options.Select(opt => new EventOption { label = opt, useCustomColors = false }).ToList();
|
||||
DrawOptions(optionsRect, eventOptions);
|
||||
}
|
||||
curY += optionsHeight + 10f;
|
||||
|
||||
// 5. Input Area (Bottom)
|
||||
Rect inputRect = new Rect(inRect.x, inRect.yMax - inputHeight, width, inputHeight);
|
||||
_inputText = Widgets.TextField(new Rect(inputRect.x, inputRect.y, inputRect.width - 85, inputHeight), _inputText);
|
||||
if (Widgets.ButtonText(new Rect(inputRect.xMax - 80, inputRect.y, 80, inputHeight), "Wula_AI_Send".Translate()))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_inputText))
|
||||
{
|
||||
@@ -338,11 +406,96 @@ OPTIONS:
|
||||
_inputText = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Override DrawSingleOption to handle click
|
||||
protected override void DrawSingleOption(Rect rect, EventOption option)
|
||||
{
|
||||
// We need to intercept the click to call SelectOption
|
||||
// But base.DrawSingleOption calls HandleAction which executes effects.
|
||||
// Here we want to send the text to AI.
|
||||
|
||||
// Close button
|
||||
if (Widgets.ButtonText(new Rect(inRect.width - 120, inRect.height - 40, 120, 30), "CloseButton".Translate()))
|
||||
// Copy logic from base but change action
|
||||
// 水平居中选项
|
||||
float optionWidth = Mathf.Min(rect.width, Dialog_CustomDisplay.Config.optionSize.x * (rect.width / Dialog_CustomDisplay.Config.windowSize.x));
|
||||
float optionX = rect.x + (rect.width - optionWidth) / 2;
|
||||
Rect optionRect = new Rect(optionX, rect.y, optionWidth, rect.height);
|
||||
|
||||
// 保存原始状态
|
||||
Color originalColor = GUI.color;
|
||||
GameFont originalFont = Text.Font;
|
||||
Color originalTextColor = GUI.contentColor;
|
||||
TextAnchor originalAnchor = Text.Anchor;
|
||||
|
||||
try
|
||||
{
|
||||
Close();
|
||||
// 设置文本居中
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
Text.Font = GameFont.Small;
|
||||
|
||||
// AI options are always enabled
|
||||
// 使用默认自定义颜色
|
||||
DrawCustomButton(optionRect, option.label.Translate(), isEnabled: true);
|
||||
|
||||
// 添加点击处理
|
||||
if (Widgets.ButtonInvisible(optionRect))
|
||||
{
|
||||
SelectOption(option.label);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 恢复原始状态
|
||||
GUI.color = originalColor;
|
||||
Text.Font = originalFont;
|
||||
GUI.contentColor = originalTextColor;
|
||||
Text.Anchor = originalAnchor;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to draw custom button (copied from base because it's private there, wait I made it protected? No, I made DrawCustomButton private in base? Let me check)
|
||||
// I made DrawCustomButton private in base. I should have made it protected.
|
||||
// Let me check my previous apply_diff.
|
||||
// I made DrawSingleOption protected virtual.
|
||||
// But DrawCustomButton is private.
|
||||
// So I need to copy DrawCustomButton here or make it protected in base.
|
||||
// I will copy it here to be safe and avoid another diff on base.
|
||||
|
||||
private void DrawCustomButton(Rect rect, string label, bool isEnabled = true)
|
||||
{
|
||||
bool isMouseOver = Mouse.IsOver(rect);
|
||||
Color buttonColor, textColor;
|
||||
|
||||
if (!isEnabled)
|
||||
{
|
||||
buttonColor = new Color(0.15f, 0.15f, 0.15f, 0.6f);
|
||||
textColor = new Color(0.6f, 0.6f, 0.6f, 1f);
|
||||
}
|
||||
else if (isMouseOver)
|
||||
{
|
||||
buttonColor = new Color(0.6f, 0.3f, 0.3f, 1f);
|
||||
textColor = new Color(1f, 1f, 1f, 1f);
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonColor = new Color(0.5f, 0.2f, 0.2f, 1f);
|
||||
textColor = new Color(0.9f, 0.9f, 0.9f, 1f);
|
||||
}
|
||||
|
||||
GUI.color = buttonColor;
|
||||
Widgets.DrawBoxSolid(rect, buttonColor);
|
||||
|
||||
if (isEnabled) Widgets.DrawBox(rect, 1);
|
||||
else Widgets.DrawBox(rect, 1);
|
||||
|
||||
GUI.color = textColor;
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
Widgets.Label(rect.ContractedBy(4f), label);
|
||||
|
||||
if (!isEnabled)
|
||||
{
|
||||
GUI.color = new Color(0.6f, 0.6f, 0.6f, 0.8f);
|
||||
Widgets.DrawLine(new Vector2(rect.x + 10f, rect.center.y), new Vector2(rect.xMax - 10f, rect.center.y), GUI.color, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
public class Dialog_CustomDisplay : Window
|
||||
{
|
||||
private EventDef def;
|
||||
private Texture2D portrait;
|
||||
private Texture2D background;
|
||||
private string selectedDescription;
|
||||
protected EventDef def;
|
||||
protected Texture2D portrait;
|
||||
protected Texture2D background;
|
||||
protected string selectedDescription;
|
||||
|
||||
private static EventUIConfigDef config;
|
||||
protected static EventUIConfigDef config;
|
||||
public static EventUIConfigDef Config
|
||||
{
|
||||
get
|
||||
@@ -37,8 +37,8 @@ namespace WulaFallenEmpire
|
||||
private static readonly Color CustomButtonTextDisabledColor = new Color(0.6f, 0.6f, 0.6f, 1f);
|
||||
|
||||
// 滚动位置
|
||||
private Vector2 descriptionScrollPosition = Vector2.zero;
|
||||
private Vector2 optionsScrollPosition = Vector2.zero;
|
||||
protected Vector2 descriptionScrollPosition = Vector2.zero;
|
||||
protected Vector2 optionsScrollPosition = Vector2.zero;
|
||||
|
||||
// 使用配置的窗口尺寸
|
||||
public override Vector2 InitialSize
|
||||
@@ -56,8 +56,10 @@ namespace WulaFallenEmpire
|
||||
// 关键修改:使用配置控制是否暂停游戏
|
||||
this.forcePause = Config.pauseGameOnOpen;
|
||||
|
||||
this.absorbInputAroundWindow = true;
|
||||
this.absorbInputAroundWindow = false; // Allow interaction with other UI elements
|
||||
this.doCloseX = true;
|
||||
this.draggable = true; // Allow dragging
|
||||
this.resizeable = true; // Allow resizing
|
||||
|
||||
// 根据配置设置是否绘制窗口背景和阴影
|
||||
this.doWindowBackground = Config.showMainWindow;
|
||||
@@ -280,7 +282,7 @@ namespace WulaFallenEmpire
|
||||
/// <summary>
|
||||
/// 修复的描述区域滚动视图 - 只显示纵向滚动条
|
||||
/// </summary>
|
||||
private void DrawDescriptionScrollView(Rect outRect, string text)
|
||||
protected virtual void DrawDescriptionScrollView(Rect outRect, string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -315,7 +317,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
|
||||
// 绘制单个选项 - 使用自定义样式
|
||||
private void DrawSingleOption(Rect rect, EventOption option)
|
||||
protected virtual void DrawSingleOption(Rect rect, EventOption option)
|
||||
{
|
||||
string reason;
|
||||
bool conditionsMet = AreConditionsMet(option.conditions, out reason);
|
||||
@@ -500,7 +502,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
|
||||
// 绘制选项区域
|
||||
private void DrawOptions(Rect rect, List<EventOption> options)
|
||||
protected virtual void DrawOptions(Rect rect, List<EventOption> options)
|
||||
{
|
||||
if (options == null || options.Count == 0)
|
||||
return;
|
||||
@@ -599,7 +601,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleAction(List<ConditionalEffects> conditionalEffects)
|
||||
protected virtual void HandleAction(List<ConditionalEffects> conditionalEffects)
|
||||
{
|
||||
if (conditionalEffects.NullOrEmpty())
|
||||
{
|
||||
@@ -615,7 +617,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
}
|
||||
|
||||
private bool AreConditionsMet(List<ConditionBase> conditions, out string reason)
|
||||
protected bool AreConditionsMet(List<ConditionBase> conditions, out string reason)
|
||||
{
|
||||
reason = "";
|
||||
if (conditions.NullOrEmpty())
|
||||
@@ -634,7 +636,7 @@ namespace WulaFallenEmpire
|
||||
return true;
|
||||
}
|
||||
|
||||
private string GetDisabledReason(EventOption option, string reason)
|
||||
protected string GetDisabledReason(EventOption option, string reason)
|
||||
{
|
||||
if (!option.disabledReason.NullOrEmpty())
|
||||
{
|
||||
|
||||
@@ -12,9 +12,10 @@ namespace WulaFallenEmpire
|
||||
|
||||
public override void Execute(Window dialog = null)
|
||||
{
|
||||
if (!RimTalkBridge.IsRimTalkActive)
|
||||
// Check if API Key is configured in local settings
|
||||
if (string.IsNullOrEmpty(WulaFallenEmpireMod.settings.apiKey))
|
||||
{
|
||||
Messages.Message("RimTalk mod is not active. AI conversation cannot be started.", MessageTypeDefOf.RejectInput, false);
|
||||
Messages.Message("AI API Key is not configured in Mod Settings. AI conversation cannot be started.", MessageTypeDefOf.RejectInput, false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user