diff --git a/PIPELINE.md b/PIPELINE.md index f0eb467..3b220d4 100644 --- a/PIPELINE.md +++ b/PIPELINE.md @@ -97,6 +97,17 @@ Commands are sent as plain text lines. Each command receives a response. | `PING` | Check if the server is running | `PING` | | `VERSION` | Get the MSAgent-AI version | `VERSION` | +### Asterisk Action Prompts + +When using the `CHAT` command, you can wrap text in asterisks to indicate actions the character should perform. The asterisks will be removed and the text will be transformed into an instruction for the AI. + +**Examples:** +- `CHAT:*you decide to tell a story*` → AI receives: "You decide to tell a story." +- `CHAT:Hello *you wave at the user* how are you?` → AI receives: "Hello You wave at the user. how are you?" +- `CHAT:*you start dancing*` → AI receives: "You start dancing." + +This feature works in both the chat interface and the Pipeline's CHAT command. + ### Response Format - `OK:COMMAND` - Command was executed successfully - `ERROR:message` - Command failed with error message diff --git a/README.md b/README.md index 5e3b17a..8a24429 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,13 @@ dotnet build 6. Use **Manage Memories** to view and manage what the AI remembers 7. Use Speak menu to make the agent tell jokes, share thoughts, or say custom text +### Asterisk Action Prompts + +In the chat interface, you can use asterisks to instruct the AI to perform actions: +- Type `*you decide to tell a story*` to make the character decide to tell a story +- Type `Hello *you wave* how are you?` to make the character wave while greeting +- Asterisks are automatically transformed into action instructions for the AI + ## Project Structure ``` diff --git a/src/AI/OllamaClient.cs b/src/AI/OllamaClient.cs index 787724b..23e243f 100644 --- a/src/AI/OllamaClient.cs +++ b/src/AI/OllamaClient.cs @@ -17,6 +17,9 @@ public class OllamaClient : IDisposable { private readonly HttpClient _httpClient; private bool _disposed; + + // Compiled regex for asterisk action detection (performance optimization) + private static readonly Regex AsteriskActionRegex = new Regex(@"\*([^*]+)\*", RegexOptions.Compiled); public string BaseUrl { get; set; } = "http://localhost:11434"; public string Model { get; set; } = "llama2"; @@ -194,6 +197,64 @@ public static (string text, List animations) ExtractAnimations(string te return (text, animations); } + /// + /// Transforms asterisk-wrapped actions in user input into instructions for the AI. + /// For example: "*you decide to tell a story*" becomes "You decide to tell a story." + /// This allows users to prompt the AI to perform specific actions. + /// + public static string TransformAsteriskActions(string message) + { + if (string.IsNullOrEmpty(message)) + return message; + + // Match text wrapped in asterisks: *action text* + // Use compiled regex for better performance + var matches = AsteriskActionRegex.Matches(message); + + if (matches.Count == 0) + return message; + + var transformedMessage = new StringBuilder(); + int lastIndex = 0; + + foreach (Match match in matches) + { + // Add any text before this match + if (match.Index > lastIndex) + { + transformedMessage.Append(message, lastIndex, match.Index - lastIndex); + } + + // Extract the action text (without asterisks) + string actionText = match.Groups[1].Value.Trim(); + + // Only process non-empty action text + if (actionText.Length > 0) + { + // Capitalize first letter + actionText = char.ToUpper(actionText[0]) + (actionText.Length > 1 ? actionText.Substring(1) : ""); + + // Add period if not already ending with punctuation + if (!actionText.EndsWith(".") && !actionText.EndsWith("!") && !actionText.EndsWith("?")) + { + actionText += "."; + } + + transformedMessage.Append(actionText); + } + + lastIndex = match.Index + match.Length; + } + + // Add any remaining text after the last match + if (lastIndex < message.Length) + { + transformedMessage.Append(message, lastIndex, message.Length - lastIndex); + } + + return transformedMessage.ToString().Trim(); + } + /// /// Sends a chat message to Ollama and gets a response /// @@ -201,6 +262,9 @@ public async Task ChatAsync(string message, CancellationToken cancellati { try { + // Transform asterisk-wrapped actions into instructions + string transformedMessage = TransformAsteriskActions(message); + // Build the messages list with personality and history var messages = new List(); @@ -222,8 +286,8 @@ public async Task ChatAsync(string message, CancellationToken cancellati }); } - // Add the new user message - messages.Add(new { role = "user", content = message }); + // Add the new user message (transformed) + messages.Add(new { role = "user", content = transformedMessage }); var request = new { @@ -251,8 +315,8 @@ public async Task ChatAsync(string message, CancellationToken cancellati { string cleanedResponse = CleanResponse(result.Message.Content); - // Add to conversation history - _conversationHistory.Add(new ChatMessage { Role = "user", Content = message }); + // Add to conversation history (use transformed message) + _conversationHistory.Add(new ChatMessage { Role = "user", Content = transformedMessage }); _conversationHistory.Add(new ChatMessage { Role = "assistant", Content = cleanedResponse }); // Try to create a memory from this conversation @@ -286,6 +350,9 @@ public async Task ChatAsync(string message, CancellationToken cancellati public async Task GenerateRandomDialogAsync(string customPrompt = null, CancellationToken cancellationToken = default) { string prompt = customPrompt ?? "Say something short, interesting, and in-character. Use /emp/ for emphasis and optionally include an &&animation trigger."; + + // Transform asterisk-wrapped actions if present in custom prompt + prompt = TransformAsteriskActions(prompt); try {