Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions PIPELINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Expand Down
75 changes: 71 additions & 4 deletions src/AI/OllamaClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -194,13 +197,74 @@ public static (string text, List<string> animations) ExtractAnimations(string te
return (text, animations);
}

/// <summary>
/// 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.
/// </summary>
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();
}

/// <summary>
/// Sends a chat message to Ollama and gets a response
/// </summary>
public async Task<string> ChatAsync(string message, CancellationToken cancellationToken = default)
{
try
{
// Transform asterisk-wrapped actions into instructions
string transformedMessage = TransformAsteriskActions(message);

// Build the messages list with personality and history
var messages = new List<object>();

Expand All @@ -222,8 +286,8 @@ public async Task<string> 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
{
Expand Down Expand Up @@ -251,8 +315,8 @@ public async Task<string> 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
Expand Down Expand Up @@ -286,6 +350,9 @@ public async Task<string> ChatAsync(string message, CancellationToken cancellati
public async Task<string> 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
{
Expand Down