zc
This commit is contained in:
@@ -11,7 +11,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
||||
{
|
||||
public class Dialog_AIConversation : Dialog_CustomDisplay
|
||||
{
|
||||
private List<ApiMessage> _history = new List<ApiMessage>();
|
||||
private List<(string role, string message)> _history = new List<(string role, string message)>();
|
||||
private string _currentResponse = "";
|
||||
private List<string> _options = new List<string>();
|
||||
private string _inputText = "";
|
||||
@@ -36,9 +36,13 @@ Do not add any other text when using tools. Your response must be either a tool
|
||||
|
||||
**CRITICAL RULE: When the player requests resources (e.g., 'we are starving', 'give us steel'), you MUST FIRST use the 'get_colonist_status' and 'get_map_resources' tools to verify their claims. After receiving the tool results, you will then decide whether to use the 'spawn_resources' tool in your NEXT turn.**
|
||||
|
||||
**CRITICAL RULE: After a tool is executed, you will receive a message with 'role: tool'. You MUST then generate a natural language response to the user, explaining the outcome of the tool's action.**
|
||||
|
||||
If you are not using a tool, provide a normal conversational response.
|
||||
After a tool use, you will receive the result, and then you should respond to the player describing what happened.
|
||||
Generate 1-3 short, distinct response options for the player at the end of your turn, formatted as:
|
||||
OPTIONS:
|
||||
1. Option 1
|
||||
2. Option 2
|
||||
3. Option 3
|
||||
|
||||
IMPORTANT: You can change your visual expression using the 'change_expression' tool.
|
||||
Expression IDs:
|
||||
@@ -122,21 +126,22 @@ Use these expressions to match your tone and reaction to the player.
|
||||
if (!def.descriptions.NullOrEmpty())
|
||||
{
|
||||
_currentResponse = def.descriptions.RandomElement().Translate();
|
||||
_history.Add(new ApiMessage { role = "assistant", content = _currentResponse });
|
||||
await GenerateResponse(isContinuation: false, customInstruction: "The conversation has started. Please generate 3 initial response options for the player based on your greeting.");
|
||||
_history.Add(("assistant", _currentResponse));
|
||||
_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(new ApiMessage { role = "user", content = "Hello" });
|
||||
_history.Add(("user", "Hello"));
|
||||
await GenerateResponse();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var lastMessage = _history.LastOrDefault();
|
||||
if (lastMessage != null && lastMessage.role == "assistant" && lastMessage.tool_calls == null)
|
||||
var lastAIResponse = _history.LastOrDefault(x => x.role == "assistant");
|
||||
if (lastAIResponse.message != null)
|
||||
{
|
||||
ParseResponse(lastMessage.content ?? "");
|
||||
ParseResponse(lastAIResponse.message);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -159,18 +164,19 @@ Use these expressions to match your tone and reaction to the player.
|
||||
return $"{baseInstruction}\n{goodwillContext}\nIMPORTANT: You MUST reply in the following language: {language}.";
|
||||
}
|
||||
|
||||
private async Task GenerateResponse(bool isContinuation = false, string customInstruction = null)
|
||||
private async Task GenerateResponse(bool isContinuation = false)
|
||||
{
|
||||
if (!isContinuation)
|
||||
{
|
||||
if (_isThinking) return;
|
||||
_isThinking = true;
|
||||
_options.Clear();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CompressHistoryIfNeeded();
|
||||
string systemInstruction = customInstruction ?? (GetSystemInstruction() + GetToolDescriptions());
|
||||
string systemInstruction = GetSystemInstruction() + GetToolDescriptions();
|
||||
|
||||
var settings = WulaFallenEmpireMod.settings;
|
||||
if (string.IsNullOrEmpty(settings.apiKey))
|
||||
@@ -181,26 +187,32 @@ Use these expressions to match your tone and reaction to the player.
|
||||
}
|
||||
|
||||
var client = new SimpleAIClient(settings.apiKey, settings.baseUrl, settings.model);
|
||||
ApiResponse response = await client.GetChatCompletionAsync(systemInstruction, _history);
|
||||
string response = await client.GetChatCompletionAsync(systemInstruction, _history);
|
||||
|
||||
if (response == null)
|
||||
if (string.IsNullOrEmpty(response))
|
||||
{
|
||||
_currentResponse = "Wula_AI_Error_ConnectionLost".Translate();
|
||||
_isThinking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_history.Add(new ApiMessage { role = "assistant", content = response.content, tool_calls = response.tool_calls });
|
||||
|
||||
if (response.tool_calls != null && response.tool_calls.Any())
|
||||
var toolCallMatch = System.Text.RegularExpressions.Regex.Match(response, @"`(\w+)\((.*)\)`");
|
||||
if (toolCallMatch.Success)
|
||||
{
|
||||
await HandleToolUsage(response.tool_calls);
|
||||
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("["))
|
||||
{
|
||||
await HandleToolUsage(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentResponse = response.content ?? "";
|
||||
ParseResponse(_currentResponse);
|
||||
_scrollPosition.y = float.MaxValue; // Force scroll to bottom
|
||||
ParseResponse(response);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -216,14 +228,14 @@ Use these expressions to match your tone and reaction to the player.
|
||||
|
||||
private void CompressHistoryIfNeeded()
|
||||
{
|
||||
int estimatedTokens = _history.Sum(h => (h.content ?? "").Length) / CharsPerToken;
|
||||
int estimatedTokens = _history.Sum(h => h.message.Length) / CharsPerToken;
|
||||
if (estimatedTokens > MaxHistoryTokens)
|
||||
{
|
||||
int removeCount = _history.Count / 2;
|
||||
if (removeCount > 0)
|
||||
{
|
||||
_history.RemoveRange(0, removeCount);
|
||||
_history.Insert(0, new ApiMessage { role = "system", content = "[Previous conversation summarized]" });
|
||||
_history.Insert(0, ("system", "[Previous conversation summarized]"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,32 +251,100 @@ Use these expressions to match your tone and reaction to the player.
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private async Task HandleToolUsage(List<ToolCall> toolCalls)
|
||||
private async Task HandleSingleToolUsage(string toolName, string args)
|
||||
{
|
||||
foreach (var toolCall in toolCalls)
|
||||
StringBuilder combinedResults = new StringBuilder();
|
||||
var tool = _tools.FirstOrDefault(t => t.Name == toolName);
|
||||
if (tool != null)
|
||||
{
|
||||
Log.Message($"[WulaAI] Executing tool '{toolCall.function.name}' with args: {toolCall.function.arguments}");
|
||||
var tool = _tools.FirstOrDefault(t => t.Name == toolCall.function.name);
|
||||
string result;
|
||||
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] == '}')
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!toolCalls.Any())
|
||||
{
|
||||
ParseResponse(json);
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder combinedResults = new StringBuilder();
|
||||
foreach (var (toolName, args) in toolCalls)
|
||||
{
|
||||
var tool = _tools.FirstOrDefault(t => t.Name == toolName);
|
||||
if (tool != null)
|
||||
{
|
||||
result = tool.Execute(toolCall.function.arguments).Trim();
|
||||
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
|
||||
{
|
||||
result = $"Error: Tool '{toolCall.function.name}' not found.";
|
||||
Log.Error($"[WulaAI] {result}");
|
||||
string errorMsg = $"Error: Tool '{toolName}' not found.";
|
||||
Log.Error($"[WulaAI] {errorMsg}");
|
||||
combinedResults.AppendLine(errorMsg);
|
||||
}
|
||||
Log.Message($"[WulaAI] Tool '{toolCall.function.name}' returned: {result}");
|
||||
_history.Add(new ApiMessage { role = "tool", tool_call_id = toolCall.id, content = result });
|
||||
}
|
||||
|
||||
|
||||
_history.Add(("assistant", json));
|
||||
_history.Add(("tool", combinedResults.ToString()));
|
||||
await GenerateResponse(isContinuation: true);
|
||||
}
|
||||
|
||||
private void ParseResponse(string rawResponse)
|
||||
{
|
||||
_currentResponse = rawResponse.Trim();
|
||||
_currentResponse = rawResponse;
|
||||
var parts = rawResponse.Split(new[] { "OPTIONS:" }, StringSplitOptions.None);
|
||||
if (_history.Count == 0 || _history.Last().role != "assistant" || _history.Last().message != rawResponse)
|
||||
{
|
||||
_history.Add(("assistant", rawResponse));
|
||||
}
|
||||
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
_options.Clear();
|
||||
var optionsLines = parts[1].Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in optionsLines)
|
||||
{
|
||||
string opt = line.Trim();
|
||||
int dotIndex = opt.IndexOf('.');
|
||||
if (dotIndex != -1 && dotIndex < 4) opt = opt.Substring(dotIndex + 1).Trim();
|
||||
if(!string.IsNullOrEmpty(opt)) _options.Add(opt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void DoWindowContents(Rect inRect)
|
||||
@@ -289,8 +369,9 @@ Use these expressions to match your tone and reaction to the player.
|
||||
curY += nameHeight + 10f;
|
||||
|
||||
float inputHeight = 30f;
|
||||
float optionsHeight = _options.Any() ? 100f : 0f;
|
||||
float bottomMargin = 10f;
|
||||
float descriptionHeight = inRect.height - curY - inputHeight - bottomMargin;
|
||||
float descriptionHeight = inRect.height - curY - inputHeight - optionsHeight - bottomMargin;
|
||||
|
||||
Rect descriptionRect = new Rect(inRect.x, curY, width, descriptionHeight);
|
||||
DrawChatHistory(descriptionRect);
|
||||
@@ -303,6 +384,14 @@ Use these expressions to match your tone and reaction to the player.
|
||||
}
|
||||
curY += descriptionHeight + 10f;
|
||||
|
||||
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;
|
||||
|
||||
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()))
|
||||
@@ -317,60 +406,45 @@ Use these expressions to match your tone and reaction to the player.
|
||||
|
||||
private void DrawChatHistory(Rect rect)
|
||||
{
|
||||
var visibleHistory = _history.Where(e => e.role != "tool" && !string.IsNullOrEmpty(e.content)).ToList();
|
||||
var lastAiMessage = visibleHistory.LastOrDefault(e => e.role == "assistant");
|
||||
Rect viewRect = new Rect(0f, 0f, rect.width - 16f, 0f);
|
||||
float tempY = 0f;
|
||||
|
||||
float totalHeight = 0f;
|
||||
foreach (var entry in visibleHistory)
|
||||
Text.Font = GameFont.Small;
|
||||
foreach (var entry in _history)
|
||||
{
|
||||
GameFont originalFont = Text.Font;
|
||||
bool isLastAiMsg = entry == lastAiMessage;
|
||||
Text.Font = isLastAiMsg ? GameFont.Medium : GameFont.Small;
|
||||
|
||||
string text = entry.content;
|
||||
totalHeight += Text.CalcHeight(text, rect.width - 16f) + 10f;
|
||||
|
||||
Text.Font = originalFont;
|
||||
if (entry.role == "tool") continue;
|
||||
string text = entry.role == "assistant" ? ParseResponseForDisplay(entry.message) : entry.message;
|
||||
tempY += Text.CalcHeight(text, viewRect.width) + 10f;
|
||||
}
|
||||
viewRect.height = tempY;
|
||||
|
||||
Rect viewRect = new Rect(0f, 0f, rect.width - 16f, totalHeight);
|
||||
Widgets.BeginScrollView(rect, ref _scrollPosition, viewRect);
|
||||
|
||||
float curY = 0f;
|
||||
for (int i = 0; i < visibleHistory.Count; i++)
|
||||
foreach (var entry in _history)
|
||||
{
|
||||
var entry = visibleHistory[i];
|
||||
|
||||
GameFont originalFont = Text.Font;
|
||||
bool isLastAiMsg = entry == lastAiMessage;
|
||||
Text.Font = isLastAiMsg ? GameFont.Medium : GameFont.Small;
|
||||
if (entry.role == "tool") continue;
|
||||
|
||||
string text = entry.content;
|
||||
string text = entry.role == "assistant" ? ParseResponseForDisplay(entry.message) : entry.message;
|
||||
float height = Text.CalcHeight(text, viewRect.width);
|
||||
Rect labelRect = new Rect(0f, curY, viewRect.width, height);
|
||||
|
||||
string label;
|
||||
if (entry.role == "user")
|
||||
{
|
||||
label = $"<color=lightblue>你: {text}</color>";
|
||||
Text.Anchor = TextAnchor.MiddleLeft;
|
||||
Widgets.Label(labelRect, $"<color=lightblue>你: {text}</color>");
|
||||
}
|
||||
else // assistant
|
||||
else
|
||||
{
|
||||
label = $"P.I.A: {text}";
|
||||
Text.Anchor = TextAnchor.MiddleLeft;
|
||||
Widgets.Label(labelRect, $"P.I.A: {text}");
|
||||
}
|
||||
|
||||
Widgets.Label(labelRect, label);
|
||||
curY += height + 10f; // Use calculated height for spacing
|
||||
|
||||
Text.Font = originalFont;
|
||||
}
|
||||
|
||||
if (Event.current.type == EventType.Layout)
|
||||
{
|
||||
_scrollPosition.y = viewRect.height;
|
||||
curY += height + 10f;
|
||||
}
|
||||
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
Widgets.EndScrollView();
|
||||
Text.Font = GameFont.Medium;
|
||||
}
|
||||
|
||||
private string ParseResponseForDisplay(string rawResponse)
|
||||
@@ -378,10 +452,76 @@ Use these expressions to match your tone and reaction to the player.
|
||||
return rawResponse.Split(new[] { "OPTIONS:" }, StringSplitOptions.None)[0].Trim();
|
||||
}
|
||||
|
||||
protected override void DrawSingleOption(Rect rect, EventOption option)
|
||||
{
|
||||
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);
|
||||
|
||||
var originalColor = GUI.color;
|
||||
var originalFont = Text.Font;
|
||||
var originalTextColor = GUI.contentColor;
|
||||
var originalAnchor = Text.Anchor;
|
||||
|
||||
try
|
||||
{
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
Text.Font = GameFont.Small;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private async void SelectOption(string text)
|
||||
{
|
||||
_history.Add(new ApiMessage { role = "user", content = text });
|
||||
_history.Add(("user", text));
|
||||
await GenerateResponse();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user